• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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.settings.notification;
18 
19 import static android.app.NotificationManager.IMPORTANCE_LOW;
20 import static android.app.NotificationManager.IMPORTANCE_NONE;
21 
22 import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
23 
24 import android.app.Notification;
25 import android.app.NotificationChannel;
26 import android.app.NotificationChannelGroup;
27 import android.app.NotificationManager;
28 import android.app.role.RoleManager;
29 import android.content.BroadcastReceiver;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.content.IntentFilter;
33 import android.content.pm.ActivityInfo;
34 import android.content.pm.PackageInfo;
35 import android.content.pm.PackageManager;
36 import android.content.pm.PackageManager.NameNotFoundException;
37 import android.content.pm.ResolveInfo;
38 import android.graphics.BlendMode;
39 import android.graphics.BlendModeColorFilter;
40 import android.graphics.drawable.Drawable;
41 import android.graphics.drawable.GradientDrawable;
42 import android.graphics.drawable.LayerDrawable;
43 import android.os.Bundle;
44 import android.os.UserHandle;
45 import android.provider.Settings;
46 import android.text.TextUtils;
47 import android.util.Log;
48 import android.widget.Toast;
49 
50 import androidx.preference.Preference;
51 import androidx.preference.PreferenceGroup;
52 import androidx.preference.PreferenceScreen;
53 
54 import com.android.settings.R;
55 import com.android.settings.SettingsActivity;
56 import com.android.settings.Utils;
57 import com.android.settings.applications.AppInfoBase;
58 import com.android.settings.core.SubSettingLauncher;
59 import com.android.settings.dashboard.DashboardFragment;
60 import com.android.settings.widget.MasterSwitchPreference;
61 import com.android.settingslib.RestrictedLockUtilsInternal;
62 
63 import java.util.ArrayList;
64 import java.util.Comparator;
65 import java.util.List;
66 
67 abstract public class NotificationSettingsBase extends DashboardFragment {
68     private static final String TAG = "NotifiSettingsBase";
69     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
70     public static final String ARG_FROM_SETTINGS = "fromSettings";
71 
72     protected PackageManager mPm;
73     protected NotificationBackend mBackend = new NotificationBackend();
74     protected NotificationManager mNm;
75     protected RoleManager mRm;
76     protected Context mContext;
77 
78     protected int mUid;
79     protected int mUserId;
80     protected String mPkg;
81     protected PackageInfo mPkgInfo;
82     protected EnforcedAdmin mSuspendedAppsAdmin;
83     protected NotificationChannelGroup mChannelGroup;
84     protected NotificationChannel mChannel;
85     protected NotificationBackend.AppRow mAppRow;
86 
87     protected boolean mShowLegacyChannelConfig = false;
88     protected boolean mListeningToPackageRemove;
89 
90     protected List<NotificationPreferenceController> mControllers = new ArrayList<>();
91     protected List<Preference> mDynamicPreferences = new ArrayList<>();
92     protected ImportanceListener mImportanceListener = new ImportanceListener();
93 
94     protected Intent mIntent;
95     protected Bundle mArgs;
96 
97     @Override
onAttach(Context context)98     public void onAttach(Context context) {
99         super.onAttach(context);
100         mContext = getActivity();
101         mIntent = getActivity().getIntent();
102         mArgs = getArguments();
103 
104         mPm = getPackageManager();
105         mNm = NotificationManager.from(mContext);
106         mRm = mContext.getSystemService(RoleManager.class);
107 
108         mPkg = mArgs != null && mArgs.containsKey(AppInfoBase.ARG_PACKAGE_NAME)
109                 ? mArgs.getString(AppInfoBase.ARG_PACKAGE_NAME)
110                 : mIntent.getStringExtra(Settings.EXTRA_APP_PACKAGE);
111         mUid = mArgs != null && mArgs.containsKey(AppInfoBase.ARG_PACKAGE_UID)
112                 ? mArgs.getInt(AppInfoBase.ARG_PACKAGE_UID)
113                 : mIntent.getIntExtra(Settings.EXTRA_APP_UID, -1);
114 
115         if (mUid < 0) {
116             try {
117                 mUid = mPm.getPackageUid(mPkg, 0);
118             } catch (NameNotFoundException e) {
119             }
120         }
121 
122         mPkgInfo = findPackageInfo(mPkg, mUid);
123 
124         if (mPkgInfo != null) {
125             mUserId = UserHandle.getUserId(mUid);
126             mSuspendedAppsAdmin = RestrictedLockUtilsInternal.checkIfApplicationIsSuspended(
127                     mContext, mPkg, mUserId);
128 
129 
130             loadChannel();
131             loadAppRow();
132             loadChannelGroup();
133             collectConfigActivities();
134 
135             getSettingsLifecycle().addObserver(use(HeaderPreferenceController.class));
136 
137             for (NotificationPreferenceController controller : mControllers) {
138                 controller.onResume(mAppRow, mChannel, mChannelGroup, mSuspendedAppsAdmin);
139             }
140         }
141     }
142 
143     @Override
onCreate(Bundle savedInstanceState)144     public void onCreate(Bundle savedInstanceState) {
145         super.onCreate(savedInstanceState);
146 
147         if (mIntent == null && mArgs == null) {
148             Log.w(TAG, "No intent");
149             toastAndFinish();
150             return;
151         }
152 
153         if (mUid < 0 || TextUtils.isEmpty(mPkg) || mPkgInfo == null) {
154             Log.w(TAG, "Missing package or uid or packageinfo");
155             toastAndFinish();
156             return;
157         }
158 
159         startListeningToPackageRemove();
160     }
161 
162     @Override
onDestroy()163     public void onDestroy() {
164         stopListeningToPackageRemove();
165         super.onDestroy();
166     }
167 
168     @Override
onResume()169     public void onResume() {
170         super.onResume();
171         if (mUid < 0 || TextUtils.isEmpty(mPkg) || mPkgInfo == null || mAppRow == null) {
172             Log.w(TAG, "Missing package or uid or packageinfo");
173             finish();
174             return;
175         }
176         // Reload app, channel, etc onResume in case they've changed. A little wasteful if we've
177         // just done onAttach but better than making every preference controller reload all
178         // the data
179         loadAppRow();
180         if (mAppRow == null) {
181             Log.w(TAG, "Can't load package");
182             finish();
183             return;
184         }
185         loadChannel();
186         loadChannelGroup();
187         collectConfigActivities();
188     }
189 
loadChannel()190     private void loadChannel() {
191         Intent intent = getActivity().getIntent();
192         String channelId = intent != null ? intent.getStringExtra(Settings.EXTRA_CHANNEL_ID) : null;
193         if (channelId == null && intent != null) {
194             Bundle args = intent.getBundleExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS);
195             channelId = args != null ? args.getString(Settings.EXTRA_CHANNEL_ID) : null;
196         }
197         mChannel = mBackend.getChannel(mPkg, mUid, channelId);
198     }
199 
loadAppRow()200     private void loadAppRow() {
201         mAppRow = mBackend.loadAppRow(mContext, mPm, mRm, mPkgInfo);
202     }
203 
loadChannelGroup()204     private void loadChannelGroup() {
205         mShowLegacyChannelConfig = mBackend.onlyHasDefaultChannel(mAppRow.pkg, mAppRow.uid)
206                 || (mChannel != null
207                 && NotificationChannel.DEFAULT_CHANNEL_ID.equals(mChannel.getId()));
208 
209         if (mShowLegacyChannelConfig) {
210             mChannel = mBackend.getChannel(
211                     mAppRow.pkg, mAppRow.uid, NotificationChannel.DEFAULT_CHANNEL_ID);
212         }
213         if (mChannel != null && !TextUtils.isEmpty(mChannel.getGroup())) {
214             NotificationChannelGroup group = mBackend.getGroup(mPkg, mUid, mChannel.getGroup());
215             if (group != null) {
216                 mChannelGroup = group;
217             }
218         }
219     }
220 
toastAndFinish()221     protected void toastAndFinish() {
222         Toast.makeText(mContext, R.string.app_not_found_dlg_text, Toast.LENGTH_SHORT).show();
223         getActivity().finish();
224     }
225 
collectConfigActivities()226     protected void collectConfigActivities() {
227         Intent intent = new Intent(Intent.ACTION_MAIN)
228                 .addCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES)
229                 .setPackage(mAppRow.pkg);
230         final List<ResolveInfo> resolveInfos = mPm.queryIntentActivities(
231                 intent,
232                 0 //PackageManager.MATCH_DEFAULT_ONLY
233         );
234         if (DEBUG) {
235             Log.d(TAG, "Found " + resolveInfos.size() + " preference activities"
236                     + (resolveInfos.size() == 0 ? " ;_;" : ""));
237         }
238         for (ResolveInfo ri : resolveInfos) {
239             final ActivityInfo activityInfo = ri.activityInfo;
240             if (mAppRow.settingsIntent != null) {
241                 if (DEBUG) {
242                     Log.d(TAG, "Ignoring duplicate notification preference activity ("
243                             + activityInfo.name + ") for package "
244                             + activityInfo.packageName);
245                 }
246                 continue;
247             }
248             // TODO(78660939): This should actually start a new task
249             mAppRow.settingsIntent = intent
250                     .setPackage(null)
251                     .setClassName(activityInfo.packageName, activityInfo.name);
252             if (mChannel != null) {
253                 mAppRow.settingsIntent.putExtra(Notification.EXTRA_CHANNEL_ID, mChannel.getId());
254             }
255             if (mChannelGroup != null) {
256                 mAppRow.settingsIntent.putExtra(
257                         Notification.EXTRA_CHANNEL_GROUP_ID, mChannelGroup.getId());
258             }
259         }
260     }
261 
findPackageInfo(String pkg, int uid)262     private PackageInfo findPackageInfo(String pkg, int uid) {
263         if (pkg == null || uid < 0) {
264             return null;
265         }
266         final String[] packages = mPm.getPackagesForUid(uid);
267         if (packages != null && pkg != null) {
268             final int N = packages.length;
269             for (int i = 0; i < N; i++) {
270                 final String p = packages[i];
271                 if (pkg.equals(p)) {
272                     try {
273                         return mPm.getPackageInfo(pkg, PackageManager.GET_SIGNATURES);
274                     } catch (NameNotFoundException e) {
275                         Log.w(TAG, "Failed to load package " + pkg, e);
276                     }
277                 }
278             }
279         }
280         return null;
281     }
282 
getAlertingIcon()283     private Drawable getAlertingIcon() {
284         Drawable icon = getContext().getDrawable(R.drawable.ic_notifications_alert);
285         icon.setTintList(Utils.getColorAccent(getContext()));
286         return icon;
287     }
288 
populateSingleChannelPrefs(PreferenceGroup parent, final NotificationChannel channel, final boolean groupBlocked)289     protected Preference populateSingleChannelPrefs(PreferenceGroup parent,
290             final NotificationChannel channel, final boolean groupBlocked) {
291         MasterSwitchPreference channelPref = new MasterSwitchPreference(getPrefContext());
292         channelPref.setSwitchEnabled(mSuspendedAppsAdmin == null
293                 && isChannelBlockable(channel)
294                 && isChannelConfigurable(channel)
295                 && !groupBlocked);
296         channelPref.setIcon(null);
297         if (channel.getImportance() > IMPORTANCE_LOW) {
298             channelPref.setIcon(getAlertingIcon());
299         }
300         channelPref.setIconSize(MasterSwitchPreference.ICON_SIZE_SMALL);
301         channelPref.setKey(channel.getId());
302         channelPref.setTitle(channel.getName());
303         channelPref.setSummary(NotificationBackend.getSentSummary(
304                 mContext, mAppRow.sentByChannel.get(channel.getId()), false));
305         channelPref.setChecked(channel.getImportance() != IMPORTANCE_NONE);
306         Bundle channelArgs = new Bundle();
307         channelArgs.putInt(AppInfoBase.ARG_PACKAGE_UID, mUid);
308         channelArgs.putString(AppInfoBase.ARG_PACKAGE_NAME, mPkg);
309         channelArgs.putString(Settings.EXTRA_CHANNEL_ID, channel.getId());
310         channelArgs.putBoolean(ARG_FROM_SETTINGS, true);
311         channelPref.setIntent(new SubSettingLauncher(getActivity())
312                 .setDestination(ChannelNotificationSettings.class.getName())
313                 .setArguments(channelArgs)
314                 .setTitleRes(R.string.notification_channel_title)
315                 .setSourceMetricsCategory(getMetricsCategory())
316                 .toIntent());
317 
318         channelPref.setOnPreferenceChangeListener(
319                 (preference, o) -> {
320                     boolean value = (Boolean) o;
321                     int importance = value ? IMPORTANCE_LOW : IMPORTANCE_NONE;
322                     channel.setImportance(importance);
323                     channel.lockFields(
324                             NotificationChannel.USER_LOCKED_IMPORTANCE);
325                     MasterSwitchPreference channelPref1 = (MasterSwitchPreference) preference;
326                     channelPref1.setIcon(null);
327                     if (channel.getImportance() > IMPORTANCE_LOW) {
328                         channelPref1.setIcon(getAlertingIcon());
329                     }
330                     toggleBehaviorIconState(channelPref1.getIcon(),
331                             importance != IMPORTANCE_NONE);
332                     mBackend.updateChannel(mPkg, mUid, channel);
333 
334                     return true;
335                 });
336         if (parent.findPreference(channelPref.getKey()) == null) {
337             parent.addPreference(channelPref);
338         }
339         return channelPref;
340     }
341 
toggleBehaviorIconState(Drawable icon, boolean enabled)342     private void toggleBehaviorIconState(Drawable icon, boolean enabled) {
343         if (icon == null) return;
344 
345         LayerDrawable layerDrawable = (LayerDrawable) icon;
346         GradientDrawable background =
347                 (GradientDrawable) layerDrawable.findDrawableByLayerId(R.id.back);
348 
349         if (background == null) return;
350 
351         if (enabled) {
352             background.clearColorFilter();
353         } else {
354             background.setColorFilter(new BlendModeColorFilter(
355                     mContext.getColor(R.color.material_grey_300),
356                     BlendMode.SRC_IN));
357         }
358     }
359 
isChannelConfigurable(NotificationChannel channel)360     protected boolean isChannelConfigurable(NotificationChannel channel) {
361         if (channel != null && mAppRow != null) {
362             return !channel.isImportanceLockedByOEM();
363         }
364         return false;
365     }
366 
isChannelBlockable(NotificationChannel channel)367     protected boolean isChannelBlockable(NotificationChannel channel) {
368         if (channel != null && mAppRow != null) {
369             if (!mAppRow.systemApp) {
370                 return true;
371             }
372 
373             if (channel.isImportanceLockedByCriticalDeviceFunction()) {
374                 return false;
375             }
376 
377             if (channel.isImportanceLockedByOEM()) {
378                 return false;
379             }
380 
381             return channel.isBlockableSystem()
382                     || channel.getImportance() == NotificationManager.IMPORTANCE_NONE;
383         }
384         return false;
385     }
386 
isChannelGroupBlockable(NotificationChannelGroup group)387     protected boolean isChannelGroupBlockable(NotificationChannelGroup group) {
388         if (group != null && mAppRow != null) {
389             if (!mAppRow.systemApp) {
390                 return true;
391             }
392 
393             return group.isBlocked();
394         }
395         return false;
396     }
397 
setVisible(Preference p, boolean visible)398     protected void setVisible(Preference p, boolean visible) {
399         setVisible(getPreferenceScreen(), p, visible);
400     }
401 
setVisible(PreferenceGroup parent, Preference p, boolean visible)402     protected void setVisible(PreferenceGroup parent, Preference p, boolean visible) {
403         final boolean isVisible = parent.findPreference(p.getKey()) != null;
404         if (isVisible == visible) return;
405         if (visible) {
406             parent.addPreference(p);
407         } else {
408             parent.removePreference(p);
409         }
410     }
411 
startListeningToPackageRemove()412     protected void startListeningToPackageRemove() {
413         if (mListeningToPackageRemove) {
414             return;
415         }
416         mListeningToPackageRemove = true;
417         final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED);
418         filter.addDataScheme("package");
419         getContext().registerReceiver(mPackageRemovedReceiver, filter);
420     }
421 
stopListeningToPackageRemove()422     protected void stopListeningToPackageRemove() {
423         if (!mListeningToPackageRemove) {
424             return;
425         }
426         mListeningToPackageRemove = false;
427         getContext().unregisterReceiver(mPackageRemovedReceiver);
428     }
429 
onPackageRemoved()430     protected void onPackageRemoved() {
431         getActivity().finishAndRemoveTask();
432     }
433 
434     protected final BroadcastReceiver mPackageRemovedReceiver = new BroadcastReceiver() {
435         @Override
436         public void onReceive(Context context, Intent intent) {
437             String packageName = intent.getData().getSchemeSpecificPart();
438             if (mPkgInfo == null || TextUtils.equals(mPkgInfo.packageName, packageName)) {
439                 if (DEBUG) {
440                     Log.d(TAG, "Package (" + packageName + ") removed. Removing"
441                             + "NotificationSettingsBase.");
442                 }
443                 onPackageRemoved();
444             }
445         }
446     };
447 
448     protected Comparator<NotificationChannel> mChannelComparator =
449             (left, right) -> {
450                 if (left.isDeleted() != right.isDeleted()) {
451                     return Boolean.compare(left.isDeleted(), right.isDeleted());
452                 } else if (left.getId().equals(NotificationChannel.DEFAULT_CHANNEL_ID)) {
453                     // Uncategorized/miscellaneous legacy channel goes last
454                     return 1;
455                 } else if (right.getId().equals(NotificationChannel.DEFAULT_CHANNEL_ID)) {
456                     return -1;
457                 }
458 
459                 return left.getId().compareTo(right.getId());
460             };
461 
462     protected class ImportanceListener {
onImportanceChanged()463         protected void onImportanceChanged() {
464             final PreferenceScreen screen = getPreferenceScreen();
465             for (NotificationPreferenceController controller : mControllers) {
466                 controller.displayPreference(screen);
467             }
468             updatePreferenceStates();
469 
470             boolean hideDynamicFields = false;
471             if (mAppRow == null || mAppRow.banned) {
472                 hideDynamicFields = true;
473             } else {
474                 if (mChannel != null) {
475                     hideDynamicFields = mChannel.getImportance() == IMPORTANCE_NONE;
476                 } else if (mChannelGroup != null) {
477                     hideDynamicFields = mChannelGroup.isBlocked();
478                 }
479             }
480             for (Preference preference : mDynamicPreferences) {
481                 setVisible(getPreferenceScreen(), preference, !hideDynamicFields);
482             }
483         }
484     }
485 }
486