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