1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.statusbar.notification.row;
18 
19 import static android.service.notification.NotificationAssistantService.FEEDBACK_RATING;
20 
21 import static com.android.systemui.statusbar.notification.AssistantFeedbackController.STATUS_ALERTED;
22 import static com.android.systemui.statusbar.notification.AssistantFeedbackController.STATUS_DEMOTED;
23 import static com.android.systemui.statusbar.notification.AssistantFeedbackController.STATUS_PROMOTED;
24 import static com.android.systemui.statusbar.notification.AssistantFeedbackController.STATUS_SILENCED;
25 
26 import android.annotation.SuppressLint;
27 import android.content.Context;
28 import android.content.pm.ApplicationInfo;
29 import android.content.pm.PackageManager;
30 import android.graphics.drawable.Drawable;
31 import android.os.Bundle;
32 import android.os.RemoteException;
33 import android.service.notification.NotificationListenerService;
34 import android.service.notification.StatusBarNotification;
35 import android.text.Html;
36 import android.util.AttributeSet;
37 import android.util.Log;
38 import android.view.View;
39 import android.view.accessibility.AccessibilityEvent;
40 import android.widget.ImageView;
41 import android.widget.LinearLayout;
42 import android.widget.TextView;
43 
44 import com.android.internal.statusbar.IStatusBarService;
45 import com.android.systemui.Dependency;
46 import com.android.systemui.R;
47 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
48 import com.android.systemui.statusbar.notification.AssistantFeedbackController;
49 import com.android.systemui.statusbar.notification.NotificationEntryManager;
50 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
51 
52 public class FeedbackInfo extends LinearLayout implements NotificationGuts.GutsContent {
53 
54     private static final String TAG = "FeedbackInfo";
55     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
56 
57     private NotificationGuts mGutsContainer;
58     private NotificationListenerService.Ranking mRanking;
59     private PackageManager mPm;
60     private String mAppName;
61     private String mPkg;
62     private NotificationEntry mEntry;
63 
64     private NotificationEntryManager mNotificationEntryManager;
65     private IStatusBarService mStatusBarService;
66     private AssistantFeedbackController mFeedbackController;
67     private NotificationGutsManager mNotificationGutsManager;
68     private NotificationMenuRowPlugin mMenuRowPlugin;
69     private ExpandableNotificationRow mExpandableNotificationRow;
70 
FeedbackInfo(Context context, AttributeSet attrs)71     public FeedbackInfo(Context context, AttributeSet attrs) {
72         super(context, attrs);
73     }
74 
bindGuts( final PackageManager pm, final StatusBarNotification sbn, final NotificationEntry entry, final ExpandableNotificationRow row, final AssistantFeedbackController controller)75     public void bindGuts(
76             final PackageManager pm,
77             final StatusBarNotification sbn,
78             final NotificationEntry entry,
79             final ExpandableNotificationRow row,
80             final AssistantFeedbackController controller) {
81         mPkg = sbn.getPackageName();
82         mPm = pm;
83         mEntry = entry;
84         mExpandableNotificationRow = row;
85         mRanking = entry.getRanking();
86         mFeedbackController = controller;
87         mAppName = mPkg;
88         mNotificationEntryManager = Dependency.get(NotificationEntryManager.class);
89         mStatusBarService = Dependency.get(IStatusBarService.class);
90         mNotificationGutsManager = Dependency.get(NotificationGutsManager.class);
91 
92         bindHeader();
93         bindPrompt();
94     }
95 
bindHeader()96     private void bindHeader() {
97         // Package name
98         Drawable pkgicon = null;
99         ApplicationInfo info;
100         try {
101             info = mPm.getApplicationInfo(mPkg,
102                     PackageManager.MATCH_UNINSTALLED_PACKAGES
103                             | PackageManager.MATCH_DISABLED_COMPONENTS
104                             | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
105                             | PackageManager.MATCH_DIRECT_BOOT_AWARE);
106             if (info != null) {
107                 mAppName = String.valueOf(mPm.getApplicationLabel(info));
108                 pkgicon = mPm.getApplicationIcon(info);
109             }
110         } catch (PackageManager.NameNotFoundException e) {
111             // app is gone, just show package name and generic icon
112             pkgicon = mPm.getDefaultActivityIcon();
113         }
114         ((ImageView) findViewById(R.id.pkg_icon)).setImageDrawable(pkgicon);
115         ((TextView) findViewById(R.id.pkg_name)).setText(mAppName);
116     }
117 
118     @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)119     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
120         super.onInitializeAccessibilityEvent(event);
121         if (mGutsContainer != null
122                 && event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
123             if (mGutsContainer.isExposed()) {
124                 event.getText().add(mContext.getString(
125                         R.string.notification_channel_controls_opened_accessibility, mAppName));
126             } else {
127                 event.getText().add(mContext.getString(
128                         R.string.notification_channel_controls_closed_accessibility, mAppName));
129             }
130         }
131     }
132 
bindPrompt()133     private void bindPrompt() {
134         final TextView prompt = findViewById(R.id.prompt);
135         final TextView yes = findViewById(R.id.yes);
136         final TextView no = findViewById(R.id.no);
137         yes.setVisibility(View.VISIBLE);
138         no.setVisibility(View.VISIBLE);
139         yes.setOnClickListener(this::positiveFeedback);
140         no.setOnClickListener(this::negativeFeedback);
141         prompt.setText(Html.fromHtml(getPrompt()));
142     }
143 
144     @SuppressLint("DefaultLocale")
getPrompt()145     private String getPrompt() {
146         StringBuilder sb = new StringBuilder();
147         int status = mFeedbackController.getFeedbackStatus(mEntry);
148         if (DEBUG) {
149             sb.append(String.format(
150                     "[DEBUG]: oldImportance=%d, newImportance=%d, ranking=%f\n\n",
151                     mRanking.getChannel().getImportance(), mRanking.getImportance(),
152                     mRanking.getRankingScore()));
153         }
154         if (status == STATUS_ALERTED) {
155             sb.append(mContext.getText(R.string.feedback_alerted));
156         } else if (status == STATUS_SILENCED) {
157             sb.append(mContext.getText(R.string.feedback_silenced));
158         } else if (status == STATUS_PROMOTED) {
159             sb.append(mContext.getText(R.string.feedback_promoted));
160         } else if (status == STATUS_DEMOTED) {
161             sb.append(mContext.getText(R.string.feedback_demoted));
162         }
163         sb.append(" ");
164         sb.append(mContext.getText(R.string.feedback_prompt));
165 
166         return sb.toString();
167     }
168 
positiveFeedback(View v)169     private void positiveFeedback(View v) {
170         mGutsContainer.closeControls(v, false);
171         handleFeedback(true);
172     }
173 
negativeFeedback(View v)174     private void negativeFeedback(View v) {
175         mMenuRowPlugin = mExpandableNotificationRow.getProvider();
176         NotificationMenuRowPlugin.MenuItem menuItem = null;
177         if (mMenuRowPlugin != null) {
178             menuItem = mMenuRowPlugin.getLongpressMenuItem(mContext);
179         }
180 
181         mGutsContainer.closeControls(v, false);
182         mNotificationGutsManager.openGuts(mExpandableNotificationRow, 0, 0, menuItem);
183         handleFeedback(false);
184     }
185 
handleFeedback(boolean positive)186     private void handleFeedback(boolean positive) {
187         Bundle feedback = new Bundle();
188         feedback.putInt(FEEDBACK_RATING, positive ? 1 : -1);
189 
190         sendFeedbackToAssistant(feedback);
191     }
192 
sendFeedbackToAssistant(Bundle feedback)193     private void sendFeedbackToAssistant(Bundle feedback) {
194         if (!mFeedbackController.isFeedbackEnabled()) {
195             return;
196         }
197 
198         try {
199             mStatusBarService.onNotificationFeedbackReceived(mRanking.getKey(), feedback);
200         } catch (RemoteException e) {
201             if (DEBUG) {
202                 Log.e(TAG, "Failed to send feedback to assistant", e);
203             }
204         }
205     }
206 
closeControls(View v)207     private void closeControls(View v) {
208         mGutsContainer.closeControls(v, false);
209     }
210 
211     @Override
setGutsParent(NotificationGuts guts)212     public void setGutsParent(NotificationGuts guts) {
213         mGutsContainer = guts;
214     }
215 
216     @Override
getContentView()217     public View getContentView() {
218         return this;
219     }
220 
221     @Override
getActualHeight()222     public int getActualHeight() {
223         return getHeight();
224     }
225 
226     @Override
handleCloseControls(boolean save, boolean force)227     public boolean handleCloseControls(boolean save, boolean force) {
228         return false;
229     }
230 
231     @Override
willBeRemoved()232     public boolean willBeRemoved() {
233         return false;
234     }
235 
236     @Override
shouldBeSaved()237     public boolean shouldBeSaved() {
238         return false;
239     }
240 
241     @Override
needsFalsingProtection()242     public boolean needsFalsingProtection() {
243         return false;
244     }
245 }
246