1 /*
2  * Copyright (C) 2017 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;
18 
19 import static android.app.NotificationManager.IMPORTANCE_NONE;
20 
21 import android.app.INotificationManager;
22 import android.app.Notification;
23 import android.app.NotificationChannel;
24 import android.app.NotificationChannelGroup;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.pm.ActivityInfo;
28 import android.content.pm.ApplicationInfo;
29 import android.content.pm.PackageInfo;
30 import android.content.pm.PackageManager;
31 import android.content.pm.ResolveInfo;
32 import android.graphics.drawable.Drawable;
33 import android.os.RemoteException;
34 import android.service.notification.StatusBarNotification;
35 import android.text.TextUtils;
36 import android.util.AttributeSet;
37 import android.view.View;
38 import android.view.accessibility.AccessibilityEvent;
39 import android.widget.ImageView;
40 import android.widget.LinearLayout;
41 import android.widget.Switch;
42 import android.widget.TextView;
43 
44 import com.android.internal.logging.MetricsLogger;
45 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
46 import com.android.settingslib.Utils;
47 import com.android.systemui.R;
48 
49 import java.lang.IllegalArgumentException;
50 import java.util.List;
51 import java.util.Set;
52 
53 /**
54  * The guts of a notification revealed when performing a long press.
55  */
56 public class NotificationInfo extends LinearLayout implements NotificationGuts.GutsContent {
57     private static final String TAG = "InfoGuts";
58 
59     private INotificationManager mINotificationManager;
60     private String mPkg;
61     private String mAppName;
62     private int mAppUid;
63     private List<NotificationChannel> mNotificationChannels;
64     private NotificationChannel mSingleNotificationChannel;
65     private boolean mIsSingleDefaultChannel;
66     private StatusBarNotification mSbn;
67     private int mStartingUserImportance;
68 
69     private TextView mNumChannelsView;
70     private View mChannelDisabledView;
71     private TextView mSettingsLinkView;
72     private Switch mChannelEnabledSwitch;
73     private CheckSaveListener mCheckSaveListener;
74     private OnAppSettingsClickListener mAppSettingsClickListener;
75     private PackageManager mPm;
76 
77     private NotificationGuts mGutsContainer;
78 
NotificationInfo(Context context, AttributeSet attrs)79     public NotificationInfo(Context context, AttributeSet attrs) {
80         super(context, attrs);
81     }
82 
83     // Specify a CheckSaveListener to override when/if the user's changes are committed.
84     public interface CheckSaveListener {
85         // Invoked when importance has changed and the NotificationInfo wants to try to save it.
86         // Listener should run saveImportance unless the change should be canceled.
checkSave(Runnable saveImportance)87         void checkSave(Runnable saveImportance);
88     }
89 
90     public interface OnSettingsClickListener {
onClick(View v, NotificationChannel channel, int appUid)91         void onClick(View v, NotificationChannel channel, int appUid);
92     }
93 
94     public interface OnAppSettingsClickListener {
onClick(View v, Intent intent)95         void onClick(View v, Intent intent);
96     }
97 
bindNotification(final PackageManager pm, final INotificationManager iNotificationManager, final String pkg, final List<NotificationChannel> notificationChannels, int startingUserImportance, final StatusBarNotification sbn, OnSettingsClickListener onSettingsClick, OnAppSettingsClickListener onAppSettingsClick, OnClickListener onDoneClick, CheckSaveListener checkSaveListener, final Set<String> nonBlockablePkgs)98     public void bindNotification(final PackageManager pm,
99             final INotificationManager iNotificationManager,
100             final String pkg,
101             final List<NotificationChannel> notificationChannels,
102             int startingUserImportance,
103             final StatusBarNotification sbn,
104             OnSettingsClickListener onSettingsClick,
105             OnAppSettingsClickListener onAppSettingsClick,
106             OnClickListener onDoneClick,
107             CheckSaveListener checkSaveListener,
108             final Set<String> nonBlockablePkgs)
109             throws RemoteException {
110         mINotificationManager = iNotificationManager;
111         mPkg = pkg;
112         mNotificationChannels = notificationChannels;
113         mCheckSaveListener = checkSaveListener;
114         mSbn = sbn;
115         mPm = pm;
116         mAppSettingsClickListener = onAppSettingsClick;
117         mStartingUserImportance = startingUserImportance;
118         mAppName = mPkg;
119         Drawable pkgicon = null;
120         CharSequence channelNameText = "";
121         ApplicationInfo info = null;
122         try {
123             info = pm.getApplicationInfo(mPkg,
124                     PackageManager.MATCH_UNINSTALLED_PACKAGES
125                             | PackageManager.MATCH_DISABLED_COMPONENTS
126                             | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
127                             | PackageManager.MATCH_DIRECT_BOOT_AWARE);
128             if (info != null) {
129                 mAppUid = info.uid;
130                 mAppName = String.valueOf(pm.getApplicationLabel(info));
131                 pkgicon = pm.getApplicationIcon(info);
132             }
133         } catch (PackageManager.NameNotFoundException e) {
134             // app is gone, just show package name and generic icon
135             pkgicon = pm.getDefaultActivityIcon();
136         }
137         ((ImageView) findViewById(R.id.pkgicon)).setImageDrawable(pkgicon);
138 
139         int numTotalChannels = iNotificationManager.getNumNotificationChannelsForPackage(
140                 pkg, mAppUid, false /* includeDeleted */);
141         if (mNotificationChannels.isEmpty()) {
142             throw new IllegalArgumentException("bindNotification requires at least one channel");
143         } else  {
144             if (mNotificationChannels.size() == 1) {
145                 mSingleNotificationChannel = mNotificationChannels.get(0);
146                 // Special behavior for the Default channel if no other channels have been defined.
147                 mIsSingleDefaultChannel =
148                         (mSingleNotificationChannel.getId()
149                                 .equals(NotificationChannel.DEFAULT_CHANNEL_ID) &&
150                         numTotalChannels <= 1);
151             } else {
152                 mSingleNotificationChannel = null;
153                 mIsSingleDefaultChannel = false;
154             }
155         }
156 
157         String channelsDescText;
158         mNumChannelsView = findViewById(R.id.num_channels_desc);
159         if (mIsSingleDefaultChannel) {
160             channelsDescText = mContext.getString(R.string.notification_default_channel_desc);
161         } else {
162             switch (mNotificationChannels.size()) {
163                 case 1:
164                     channelsDescText = String.format(mContext.getResources().getQuantityString(
165                             R.plurals.notification_num_channels_desc, numTotalChannels),
166                             numTotalChannels);
167                     break;
168                 case 2:
169                     channelsDescText = mContext.getString(
170                             R.string.notification_channels_list_desc_2,
171                             mNotificationChannels.get(0).getName(),
172                             mNotificationChannels.get(1).getName());
173                     break;
174                 default:
175                     final int numOthers = mNotificationChannels.size() - 2;
176                     channelsDescText = String.format(
177                             mContext.getResources().getQuantityString(
178                                     R.plurals.notification_channels_list_desc_2_and_others,
179                                     numOthers),
180                             mNotificationChannels.get(0).getName(),
181                             mNotificationChannels.get(1).getName(),
182                             numOthers);
183             }
184         }
185         mNumChannelsView.setText(channelsDescText);
186 
187         if (mSingleNotificationChannel == null) {
188             // Multiple channels don't use a channel name for the title.
189             channelNameText = mContext.getString(R.string.notification_num_channels,
190                     mNotificationChannels.size());
191         } else if (mIsSingleDefaultChannel) {
192             // If this is the default channel, don't use our channel-specific text.
193             channelNameText = mContext.getString(R.string.notification_header_default_channel);
194         } else {
195             channelNameText = mSingleNotificationChannel.getName();
196         }
197         ((TextView) findViewById(R.id.pkgname)).setText(mAppName);
198         ((TextView) findViewById(R.id.channel_name)).setText(channelNameText);
199 
200         // Set group information if this channel has an associated group.
201         CharSequence groupName = null;
202         if (mSingleNotificationChannel != null && mSingleNotificationChannel.getGroup() != null) {
203             final NotificationChannelGroup notificationChannelGroup =
204                     iNotificationManager.getNotificationChannelGroupForPackage(
205                             mSingleNotificationChannel.getGroup(), pkg, mAppUid);
206             if (notificationChannelGroup != null) {
207                 groupName = notificationChannelGroup.getName();
208             }
209         }
210         TextView groupNameView = ((TextView) findViewById(R.id.group_name));
211         TextView groupDividerView = ((TextView) findViewById(R.id.pkg_group_divider));
212         if (groupName != null) {
213             groupNameView.setText(groupName);
214             groupNameView.setVisibility(View.VISIBLE);
215             groupDividerView.setVisibility(View.VISIBLE);
216         } else {
217             groupNameView.setVisibility(View.GONE);
218             groupDividerView.setVisibility(View.GONE);
219         }
220 
221         boolean nonBlockable = false;
222         try {
223             final PackageInfo pkgInfo = pm.getPackageInfo(pkg, PackageManager.GET_SIGNATURES);
224             nonBlockable = Utils.isSystemPackage(getResources(), pm, pkgInfo)
225                     && (mSingleNotificationChannel == null
226                     || !mSingleNotificationChannel.isBlockableSystem());
227         } catch (PackageManager.NameNotFoundException e) {
228             // unlikely.
229         }
230         if (nonBlockablePkgs != null) {
231             nonBlockable |= nonBlockablePkgs.contains(pkg);
232         }
233 
234         bindButtons(nonBlockable);
235 
236         // Top-level importance group
237         mChannelDisabledView = findViewById(R.id.channel_disabled);
238         updateSecondaryText();
239 
240         // Settings button.
241         final TextView settingsButton = (TextView) findViewById(R.id.more_settings);
242         if (mAppUid >= 0 && onSettingsClick != null) {
243             settingsButton.setVisibility(View.VISIBLE);
244             final int appUidF = mAppUid;
245             settingsButton.setOnClickListener(
246                     (View view) -> {
247                         onSettingsClick.onClick(view, mSingleNotificationChannel, appUidF);
248                     });
249             if (numTotalChannels > 1) {
250                 settingsButton.setText(R.string.notification_all_categories);
251             } else {
252                 settingsButton.setText(R.string.notification_more_settings);
253             }
254 
255         } else {
256             settingsButton.setVisibility(View.GONE);
257         }
258 
259         // Done button.
260         final TextView doneButton = (TextView) findViewById(R.id.done);
261         doneButton.setText(R.string.notification_done);
262         doneButton.setOnClickListener(onDoneClick);
263 
264         // Optional settings link
265         updateAppSettingsLink();
266     }
267 
hasImportanceChanged()268     private boolean hasImportanceChanged() {
269         return mSingleNotificationChannel != null &&
270                 mChannelEnabledSwitch != null &&
271                 mStartingUserImportance != getSelectedImportance();
272     }
273 
saveImportance()274     private void saveImportance() {
275         if (!hasImportanceChanged()) {
276             return;
277         }
278         final int selectedImportance = getSelectedImportance();
279         MetricsLogger.action(mContext, MetricsEvent.ACTION_SAVE_IMPORTANCE,
280                 selectedImportance - mStartingUserImportance);
281         mSingleNotificationChannel.setImportance(selectedImportance);
282         mSingleNotificationChannel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
283         try {
284             mINotificationManager.updateNotificationChannelForPackage(
285                     mPkg, mAppUid, mSingleNotificationChannel);
286         } catch (RemoteException e) {
287             // :(
288         }
289     }
290 
getSelectedImportance()291     private int getSelectedImportance() {
292         if (!mChannelEnabledSwitch.isChecked()) {
293             return IMPORTANCE_NONE;
294         } else {
295             return mStartingUserImportance;
296         }
297     }
298 
bindButtons(final boolean nonBlockable)299     private void bindButtons(final boolean nonBlockable) {
300         // Enabled Switch
301         mChannelEnabledSwitch = (Switch) findViewById(R.id.channel_enabled_switch);
302         mChannelEnabledSwitch.setChecked(
303                 mStartingUserImportance != IMPORTANCE_NONE);
304         final boolean visible = !nonBlockable && mSingleNotificationChannel != null;
305         mChannelEnabledSwitch.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
306 
307         // Callback when checked.
308         mChannelEnabledSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
309             if (mGutsContainer != null) {
310                 mGutsContainer.resetFalsingCheck();
311             }
312             updateSecondaryText();
313             updateAppSettingsLink();
314         });
315     }
316 
317     @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)318     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
319         super.onInitializeAccessibilityEvent(event);
320         if (mGutsContainer != null &&
321                 event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
322             if (mGutsContainer.isExposed()) {
323                 event.getText().add(mContext.getString(
324                         R.string.notification_channel_controls_opened_accessibility, mAppName));
325             } else {
326                 event.getText().add(mContext.getString(
327                         R.string.notification_channel_controls_closed_accessibility, mAppName));
328             }
329         }
330     }
331 
updateSecondaryText()332     private void updateSecondaryText() {
333         final boolean disabled = mSingleNotificationChannel != null &&
334                 getSelectedImportance() == IMPORTANCE_NONE;
335         if (disabled) {
336             mChannelDisabledView.setVisibility(View.VISIBLE);
337             mNumChannelsView.setVisibility(View.GONE);
338         } else {
339             mChannelDisabledView.setVisibility(View.GONE);
340             mNumChannelsView.setVisibility(mIsSingleDefaultChannel ? View.INVISIBLE : View.VISIBLE);
341         }
342     }
343 
updateAppSettingsLink()344     private void updateAppSettingsLink() {
345         mSettingsLinkView = findViewById(R.id.app_settings);
346         Intent settingsIntent = getAppSettingsIntent(mPm, mPkg, mSingleNotificationChannel,
347                 mSbn.getId(), mSbn.getTag());
348         if (settingsIntent != null && getSelectedImportance() != IMPORTANCE_NONE
349                 && !TextUtils.isEmpty(mSbn.getNotification().getSettingsText())) {
350             mSettingsLinkView.setVisibility(View.VISIBLE);
351             mSettingsLinkView.setText(mContext.getString(R.string.notification_app_settings,
352                     mSbn.getNotification().getSettingsText()));
353             mSettingsLinkView.setOnClickListener((View view) -> {
354                 mAppSettingsClickListener.onClick(view, settingsIntent);
355             });
356         } else {
357             mSettingsLinkView.setVisibility(View.GONE);
358         }
359     }
360 
getAppSettingsIntent(PackageManager pm, String packageName, NotificationChannel channel, int id, String tag)361     private Intent getAppSettingsIntent(PackageManager pm, String packageName,
362             NotificationChannel channel, int id, String tag) {
363         Intent intent = new Intent(Intent.ACTION_MAIN)
364                 .addCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES)
365                 .setPackage(packageName);
366         final List<ResolveInfo> resolveInfos = pm.queryIntentActivities(
367                 intent,
368                 PackageManager.MATCH_DEFAULT_ONLY
369         );
370         if (resolveInfos == null || resolveInfos.size() == 0 || resolveInfos.get(0) == null) {
371             return null;
372         }
373         final ActivityInfo activityInfo = resolveInfos.get(0).activityInfo;
374         intent.setClassName(activityInfo.packageName, activityInfo.name);
375         if (channel != null) {
376             intent.putExtra(Notification.EXTRA_CHANNEL_ID, channel.getId());
377         }
378         intent.putExtra(Notification.EXTRA_NOTIFICATION_ID, id);
379         intent.putExtra(Notification.EXTRA_NOTIFICATION_TAG, tag);
380         return intent;
381     }
382 
383     @Override
setGutsParent(NotificationGuts guts)384     public void setGutsParent(NotificationGuts guts) {
385         mGutsContainer = guts;
386     }
387 
388     @Override
willBeRemoved()389     public boolean willBeRemoved() {
390         return mChannelEnabledSwitch != null && !mChannelEnabledSwitch.isChecked();
391     }
392 
393     @Override
getContentView()394     public View getContentView() {
395         return this;
396     }
397 
398     @Override
handleCloseControls(boolean save, boolean force)399     public boolean handleCloseControls(boolean save, boolean force) {
400         if (save && hasImportanceChanged()) {
401             if (mCheckSaveListener != null) {
402                 mCheckSaveListener.checkSave(() -> { saveImportance(); });
403             } else {
404                 saveImportance();
405             }
406         }
407         return false;
408     }
409 
410     @Override
getActualHeight()411     public int getActualHeight() {
412         return getHeight();
413     }
414 }
415