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.settings.notification.zen;
18 
19 import android.app.Application;
20 import android.app.settings.SettingsEnums;
21 import android.content.Context;
22 import android.graphics.drawable.Drawable;
23 import android.os.Bundle;
24 import android.os.UserHandle;
25 
26 import androidx.annotation.VisibleForTesting;
27 import androidx.core.text.BidiFormatter;
28 import androidx.fragment.app.Fragment;
29 import androidx.preference.Preference;
30 import androidx.preference.PreferenceCategory;
31 import androidx.preference.PreferenceScreen;
32 
33 import com.android.settings.R;
34 import com.android.settings.applications.AppInfoBase;
35 import com.android.settings.core.PreferenceControllerMixin;
36 import com.android.settings.core.SubSettingLauncher;
37 import com.android.settings.notification.NotificationBackend;
38 import com.android.settings.notification.app.AppChannelsBypassingDndSettings;
39 import com.android.settingslib.applications.AppUtils;
40 import com.android.settingslib.applications.ApplicationsState;
41 import com.android.settingslib.core.AbstractPreferenceController;
42 import com.android.settingslib.utils.ThreadUtils;
43 import com.android.settingslib.widget.AppPreference;
44 
45 import java.util.ArrayList;
46 import java.util.List;
47 
48 
49 /**
50  * When clicked, populates the PreferenceScreen with apps that aren't already bypassing DND. The
51  * user can click on these Preferences to allow notification channels from the app to bypass DND.
52  */
53 public class ZenModeAddBypassingAppsPreferenceController extends AbstractPreferenceController
54         implements PreferenceControllerMixin {
55 
56     public static final String KEY_NO_APPS = "add_none";
57     private static final String KEY = "zen_mode_non_bypassing_apps_list";
58     private static final String KEY_ADD = "zen_mode_bypassing_apps_add";
59     private final NotificationBackend mNotificationBackend;
60 
61     @VisibleForTesting ApplicationsState mApplicationsState;
62     @VisibleForTesting PreferenceScreen mPreferenceScreen;
63     @VisibleForTesting PreferenceCategory mPreferenceCategory;
64     @VisibleForTesting Context mPrefContext;
65 
66     private Preference mAddPreference;
67     private ApplicationsState.Session mAppSession;
68     private Fragment mHostFragment;
69 
ZenModeAddBypassingAppsPreferenceController(Context context, Application app, Fragment host, NotificationBackend notificationBackend)70     public ZenModeAddBypassingAppsPreferenceController(Context context, Application app,
71             Fragment host, NotificationBackend notificationBackend) {
72         this(context, app == null ? null : ApplicationsState.getInstance(app), host,
73                 notificationBackend);
74     }
75 
ZenModeAddBypassingAppsPreferenceController(Context context, ApplicationsState appState, Fragment host, NotificationBackend notificationBackend)76     private ZenModeAddBypassingAppsPreferenceController(Context context, ApplicationsState appState,
77             Fragment host, NotificationBackend notificationBackend) {
78         super(context);
79         mNotificationBackend = notificationBackend;
80         mApplicationsState = appState;
81         mHostFragment = host;
82     }
83 
84     @Override
displayPreference(PreferenceScreen screen)85     public void displayPreference(PreferenceScreen screen) {
86         mPreferenceScreen = screen;
87         mAddPreference = screen.findPreference(KEY_ADD);
88         mAddPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
89             @Override
90             public boolean onPreferenceClick(Preference preference) {
91                 mAddPreference.setVisible(false);
92                 if (mApplicationsState != null && mHostFragment != null) {
93                     mAppSession = mApplicationsState.newSession(mAppSessionCallbacks,
94                             mHostFragment.getLifecycle());
95                 }
96                 return true;
97             }
98         });
99         mPrefContext = screen.getContext();
100         super.displayPreference(screen);
101     }
102 
103     @Override
isAvailable()104     public boolean isAvailable() {
105         return true;
106     }
107 
108     @Override
getPreferenceKey()109     public String getPreferenceKey() {
110         return KEY;
111     }
112 
113     /**
114      * Call this method to trigger the app list to refresh.
115      */
updateAppList()116     public void updateAppList() {
117         if (mAppSession == null) {
118             return;
119         }
120 
121         ApplicationsState.AppFilter filter = android.multiuser.Flags.enablePrivateSpaceFeatures()
122                 && android.multiuser.Flags.handleInterleavedSettingsForPrivateSpace()
123                 ? ApplicationsState.FILTER_ENABLED_NOT_QUIET
124                 : ApplicationsState.FILTER_ALL_ENABLED;
125         mAppSession.rebuild(filter, ApplicationsState.ALPHA_COMPARATOR);
126     }
127 
128     // Set the icon for the given preference to the entry icon from cache if available, or look
129     // it up.
updateIcon(Preference pref, ApplicationsState.AppEntry entry)130     private void updateIcon(Preference pref, ApplicationsState.AppEntry entry) {
131         synchronized (entry) {
132             final Drawable cachedIcon = AppUtils.getIconFromCache(entry);
133             if (cachedIcon != null && entry.mounted) {
134                 pref.setIcon(cachedIcon);
135             } else {
136                 ThreadUtils.postOnBackgroundThread(() -> {
137                     final Drawable icon = AppUtils.getIcon(mPrefContext, entry);
138                     if (icon != null) {
139                         ThreadUtils.postOnMainThread(() -> pref.setIcon(icon));
140                     }
141                 });
142             }
143         }
144     }
145 
146     @VisibleForTesting
updateAppList(List<ApplicationsState.AppEntry> apps)147     void updateAppList(List<ApplicationsState.AppEntry> apps) {
148         if (apps == null) {
149             return;
150         }
151 
152         if (mPreferenceCategory == null) {
153             mPreferenceCategory = new PreferenceCategory(mPrefContext);
154             mPreferenceCategory.setTitle(R.string.zen_mode_bypassing_apps_add_header);
155             mPreferenceScreen.addPreference(mPreferenceCategory);
156         }
157 
158         boolean doAnyAppsPassCriteria = false;
159         for (ApplicationsState.AppEntry app : apps) {
160             String pkg = app.info.packageName;
161             final String key = getKey(pkg, app.info.uid);
162             final int appChannels = mNotificationBackend.getChannelCount(pkg, app.info.uid);
163             final int appChannelsBypassingDnd = mNotificationBackend
164                     .getNotificationChannelsBypassingDnd(pkg, app.info.uid).getList().size();
165             if (appChannelsBypassingDnd == 0 && appChannels > 0) {
166                 doAnyAppsPassCriteria = true;
167             }
168 
169             Preference pref = mPreferenceCategory.findPreference(key);
170 
171             if (pref == null) {
172                 if (appChannelsBypassingDnd == 0 && appChannels > 0) {
173                     // does not exist but should
174                     pref = new AppPreference(mPrefContext);
175                     pref.setKey(key);
176                     pref.setOnPreferenceClickListener(preference -> {
177                         Bundle args = new Bundle();
178                         args.putString(AppInfoBase.ARG_PACKAGE_NAME, app.info.packageName);
179                         args.putInt(AppInfoBase.ARG_PACKAGE_UID, app.info.uid);
180                         new SubSettingLauncher(mContext)
181                                 .setDestination(AppChannelsBypassingDndSettings.class.getName())
182                                 .setArguments(args)
183                                 .setResultListener(mHostFragment, 0)
184                                 .setUserHandle(new UserHandle(UserHandle.getUserId(app.info.uid)))
185                                 .setSourceMetricsCategory(
186                                         SettingsEnums.NOTIFICATION_ZEN_MODE_OVERRIDING_APP)
187                                 .launch();
188                         return true;
189                     });
190                     pref.setTitle(BidiFormatter.getInstance().unicodeWrap(app.label));
191                     updateIcon(pref, app);
192                     mPreferenceCategory.addPreference(pref);
193                 }
194             } else if (appChannelsBypassingDnd != 0 || appChannels == 0) {
195                 // exists but shouldn't anymore
196                 mPreferenceCategory.removePreference(pref);
197             }
198         }
199 
200         Preference pref = mPreferenceCategory.findPreference(KEY_NO_APPS);
201         if (!doAnyAppsPassCriteria) {
202             if (pref == null) {
203                 pref = new Preference(mPrefContext);
204                 pref.setKey(KEY_NO_APPS);
205                 pref.setTitle(R.string.zen_mode_bypassing_apps_none);
206             }
207             mPreferenceCategory.addPreference(pref);
208         } else if (pref != null) {
209             mPreferenceCategory.removePreference(pref);
210         }
211     }
212 
getKey(String pkg, int uid)213     static String getKey(String pkg, int uid) {
214         return "add|" + pkg + "|" + uid;
215     }
216 
217     private final ApplicationsState.Callbacks mAppSessionCallbacks =
218             new ApplicationsState.Callbacks() {
219 
220                 @Override
221                 public void onRunningStateChanged(boolean running) {
222 
223                 }
224 
225                 @Override
226                 public void onPackageListChanged() {
227 
228                 }
229 
230                 @Override
231                 public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) {
232                     updateAppList(apps);
233                 }
234 
235                 @Override
236                 public void onPackageIconChanged() {
237                     updateAppList();
238                 }
239 
240                 @Override
241                 public void onPackageSizeChanged(String packageName) {
242 
243                 }
244 
245                 @Override
246                 public void onAllSizesComputed() { }
247 
248                 @Override
249                 public void onLauncherInfoChanged() {
250 
251                 }
252 
253                 @Override
254                 public void onLoadEntriesCompleted() {
255                     updateAppList();
256                 }
257             };
258 }
259