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