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.plugins.statusbar.NotificationMenuRowPlugin;
46 import com.android.systemui.res.R;
47 import com.android.systemui.statusbar.notification.AssistantFeedbackController;
48 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
49 import com.android.systemui.util.Compile;
50 
51 public class FeedbackInfo extends LinearLayout implements NotificationGuts.GutsContent {
52 
53     private static final String TAG = "FeedbackInfo";
54     private static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG);
55 
56     private NotificationGuts mGutsContainer;
57     private NotificationListenerService.Ranking mRanking;
58     private PackageManager mPm;
59     private String mAppName;
60     private String mPkg;
61     private NotificationEntry mEntry;
62 
63     private IStatusBarService mStatusBarService;
64     private AssistantFeedbackController mFeedbackController;
65     private NotificationGutsManager mNotificationGutsManager;
66     private NotificationMenuRowPlugin mMenuRowPlugin;
67     private ExpandableNotificationRow mExpandableNotificationRow;
68 
FeedbackInfo(Context context, AttributeSet attrs)69     public FeedbackInfo(Context context, AttributeSet attrs) {
70         super(context, attrs);
71     }
72 
bindGuts( final PackageManager pm, final StatusBarNotification sbn, final NotificationEntry entry, final ExpandableNotificationRow row, final AssistantFeedbackController controller, final IStatusBarService statusBarService, final NotificationGutsManager notificationGutsManager)73     public void bindGuts(
74             final PackageManager pm,
75             final StatusBarNotification sbn,
76             final NotificationEntry entry,
77             final ExpandableNotificationRow row,
78             final AssistantFeedbackController controller,
79             final IStatusBarService statusBarService,
80             final NotificationGutsManager notificationGutsManager) {
81         mPkg = sbn.getPackageName();
82         mPm = pm;
83         mEntry = entry;
84         mExpandableNotificationRow = row;
85         mRanking = entry.getRanking();
86         mFeedbackController = controller;
87         mAppName = mPkg;
88         mStatusBarService = statusBarService;
89         mNotificationGutsManager = notificationGutsManager;
90 
91         bindHeader();
92         bindPrompt();
93     }
94 
bindHeader()95     private void bindHeader() {
96         // Package name
97         Drawable pkgicon = null;
98         ApplicationInfo info;
99         try {
100             info = mPm.getApplicationInfo(mPkg,
101                     PackageManager.MATCH_UNINSTALLED_PACKAGES
102                             | PackageManager.MATCH_DISABLED_COMPONENTS
103                             | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
104                             | PackageManager.MATCH_DIRECT_BOOT_AWARE);
105             if (info != null) {
106                 mAppName = String.valueOf(mPm.getApplicationLabel(info));
107                 pkgicon = mPm.getApplicationIcon(info);
108             }
109         } catch (PackageManager.NameNotFoundException e) {
110             // app is gone, just show package name and generic icon
111             pkgicon = mPm.getDefaultActivityIcon();
112         }
113         ((ImageView) findViewById(R.id.pkg_icon)).setImageDrawable(pkgicon);
114         ((TextView) findViewById(R.id.pkg_name)).setText(mAppName);
115     }
116 
117     @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)118     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
119         super.onInitializeAccessibilityEvent(event);
120         if (mGutsContainer != null
121                 && event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
122             if (mGutsContainer.isExposed()) {
123                 event.getText().add(mContext.getString(
124                         R.string.notification_channel_controls_opened_accessibility, mAppName));
125             } else {
126                 event.getText().add(mContext.getString(
127                         R.string.notification_channel_controls_closed_accessibility, mAppName));
128             }
129         }
130     }
131 
bindPrompt()132     private void bindPrompt() {
133         final TextView prompt = findViewById(R.id.prompt);
134         final TextView yes = findViewById(R.id.yes);
135         final TextView no = findViewById(R.id.no);
136         yes.setVisibility(View.VISIBLE);
137         no.setVisibility(View.VISIBLE);
138         yes.setOnClickListener(this::positiveFeedback);
139         no.setOnClickListener(this::negativeFeedback);
140         prompt.setText(Html.fromHtml(getPrompt()));
141     }
142 
143     @SuppressLint("DefaultLocale")
getPrompt()144     private String getPrompt() {
145         StringBuilder sb = new StringBuilder();
146         int status = mFeedbackController.getFeedbackStatus(mEntry);
147         if (DEBUG) {
148             sb.append(String.format(
149                     "[DEBUG]: oldImportance=%d, newImportance=%d, ranking=%f\n\n",
150                     mRanking.getChannel().getImportance(), mRanking.getImportance(),
151                     mRanking.getRankingScore()));
152         }
153         if (status == STATUS_ALERTED) {
154             sb.append(mContext.getText(R.string.feedback_alerted));
155         } else if (status == STATUS_SILENCED) {
156             sb.append(mContext.getText(R.string.feedback_silenced));
157         } else if (status == STATUS_PROMOTED) {
158             sb.append(mContext.getText(R.string.feedback_promoted));
159         } else if (status == STATUS_DEMOTED) {
160             sb.append(mContext.getText(R.string.feedback_demoted));
161         }
162         sb.append(" ");
163         sb.append(mContext.getText(R.string.feedback_prompt));
164 
165         return sb.toString();
166     }
167 
positiveFeedback(View v)168     private void positiveFeedback(View v) {
169         mGutsContainer.closeControls(v, /* save= */ false);
170         handleFeedback(true);
171     }
172 
negativeFeedback(View v)173     private void negativeFeedback(View v) {
174         mMenuRowPlugin = mExpandableNotificationRow.getProvider();
175         NotificationMenuRowPlugin.MenuItem menuItem = null;
176         if (mMenuRowPlugin != null) {
177             menuItem = mMenuRowPlugin.getLongpressMenuItem(mContext);
178         }
179 
180         mGutsContainer.closeControls(v, /* save= */ false);
181         mNotificationGutsManager.openGuts(mExpandableNotificationRow, 0, 0, menuItem);
182         handleFeedback(false);
183     }
184 
handleFeedback(boolean positive)185     private void handleFeedback(boolean positive) {
186         Bundle feedback = new Bundle();
187         feedback.putInt(FEEDBACK_RATING, positive ? 1 : -1);
188 
189         sendFeedbackToAssistant(feedback);
190     }
191 
sendFeedbackToAssistant(Bundle feedback)192     private void sendFeedbackToAssistant(Bundle feedback) {
193         if (!mFeedbackController.isFeedbackEnabled()) {
194             return;
195         }
196 
197         try {
198             mStatusBarService.onNotificationFeedbackReceived(mRanking.getKey(), feedback);
199         } catch (RemoteException e) {
200             if (DEBUG) {
201                 Log.e(TAG, "Failed to send feedback to assistant", e);
202             }
203         }
204     }
205 
closeControls(View v)206     private void closeControls(View v) {
207         mGutsContainer.closeControls(v, /* save= */ false);
208     }
209 
210     @Override
setGutsParent(NotificationGuts guts)211     public void setGutsParent(NotificationGuts guts) {
212         mGutsContainer = guts;
213     }
214 
215     @Override
getContentView()216     public View getContentView() {
217         return this;
218     }
219 
220     @Override
getActualHeight()221     public int getActualHeight() {
222         return getHeight();
223     }
224 
225     @Override
handleCloseControls(boolean save, boolean force)226     public boolean handleCloseControls(boolean save, boolean force) {
227         return false;
228     }
229 
230     @Override
willBeRemoved()231     public boolean willBeRemoved() {
232         return false;
233     }
234 
235     @Override
shouldBeSavedOnClose()236     public boolean shouldBeSavedOnClose() {
237         return false;
238     }
239 
240     @Override
needsFalsingProtection()241     public boolean needsFalsingProtection() {
242         return false;
243     }
244 }
245