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  * Adds a preference to the PreferenceScreen for each notification channel that can bypass DND.
51  */
52 public class ZenModeAllBypassingAppsPreferenceController extends AbstractPreferenceController
53         implements PreferenceControllerMixin {
54     public static final String KEY_NO_APPS = "all_none";
55     private static final String KEY = "zen_mode_bypassing_apps_list";
56 
57     @Nullable private final NotificationBackend mNotificationBackend;
58 
59     @Nullable @VisibleForTesting ApplicationsState mApplicationsState;
60     @VisibleForTesting PreferenceCategory mPreferenceCategory;
61     @VisibleForTesting Context mPrefContext;
62 
63     private ApplicationsState.Session mAppSession;
64     @Nullable private Fragment mHostFragment;
65 
ZenModeAllBypassingAppsPreferenceController(Context context, @Nullable Application app, @Nullable Fragment host, @Nullable NotificationBackend notificationBackend)66     public ZenModeAllBypassingAppsPreferenceController(Context context, @Nullable Application app,
67             @Nullable Fragment host, @Nullable NotificationBackend notificationBackend) {
68         this(context, app == null ? null : ApplicationsState.getInstance(app), host,
69                 notificationBackend);
70     }
71 
ZenModeAllBypassingAppsPreferenceController(Context context, @Nullable ApplicationsState appState, @Nullable Fragment host, @Nullable NotificationBackend notificationBackend)72     private ZenModeAllBypassingAppsPreferenceController(Context context,
73             @Nullable ApplicationsState appState, @Nullable Fragment host,
74             @Nullable NotificationBackend notificationBackend) {
75         super(context);
76         mNotificationBackend = notificationBackend;
77         mApplicationsState = appState;
78         mHostFragment = host;
79 
80         if (mApplicationsState != null && host != null) {
81             mAppSession = mApplicationsState.newSession(mAppSessionCallbacks, host.getLifecycle());
82         }
83     }
84 
85     @Override
displayPreference(PreferenceScreen screen)86     public void displayPreference(PreferenceScreen screen) {
87         mPreferenceCategory = screen.findPreference(KEY);
88         mPrefContext = screen.getContext();
89         updateAppList();
90         super.displayPreference(screen);
91     }
92 
93     @Override
isAvailable()94     public boolean isAvailable() {
95         return true;
96     }
97 
98     @Override
getPreferenceKey()99     public String getPreferenceKey() {
100         return KEY;
101     }
102 
103     /**
104      * Call this method to trigger the app list to refresh.
105      */
updateAppList()106     public void updateAppList() {
107         if (mAppSession == null) {
108             return;
109         }
110 
111         ApplicationsState.AppFilter filter = android.multiuser.Flags.enablePrivateSpaceFeatures()
112                 && android.multiuser.Flags.handleInterleavedSettingsForPrivateSpace()
113                 ? ApplicationsState.FILTER_ENABLED_NOT_QUIET
114                 : ApplicationsState.FILTER_ALL_ENABLED;
115         mAppSession.rebuild(filter, ApplicationsState.ALPHA_COMPARATOR);
116     }
117 
118     // Set the icon for the given preference to the entry icon from cache if available, or look
119     // it up.
updateIcon(Preference pref, ApplicationsState.AppEntry entry)120     private void updateIcon(Preference pref, ApplicationsState.AppEntry entry) {
121         synchronized (entry) {
122             final Drawable cachedIcon = AppUtils.getIconFromCache(entry);
123             if (cachedIcon != null && entry.mounted) {
124                 pref.setIcon(cachedIcon);
125             } else {
126                 ThreadUtils.postOnBackgroundThread(() -> {
127                     final Drawable icon = AppUtils.getIcon(mPrefContext, entry);
128                     if (icon != null) {
129                         ThreadUtils.postOnMainThread(() -> pref.setIcon(icon));
130                     }
131                 });
132             }
133         }
134     }
135 
136     @VisibleForTesting
updateAppList(List<ApplicationsState.AppEntry> apps)137     void updateAppList(List<ApplicationsState.AppEntry> apps) {
138         if (mPreferenceCategory == null || apps == null) {
139             return;
140         }
141 
142         boolean doAnyAppsPassCriteria = false;
143         for (ApplicationsState.AppEntry app : apps) {
144             String pkg = app.info.packageName;
145             final String key = getKey(pkg, app.info.uid);
146             final int appChannels = mNotificationBackend.getChannelCount(pkg, app.info.uid);
147             final int appChannelsBypassingDnd = mNotificationBackend
148                     .getNotificationChannelsBypassingDnd(pkg, app.info.uid).getList().size();
149             if (appChannelsBypassingDnd > 0) {
150                 doAnyAppsPassCriteria = true;
151             }
152 
153             Preference pref = mPreferenceCategory.findPreference(key);
154             if (pref == null) {
155                 if (appChannelsBypassingDnd > 0) {
156                     // does not exist but should
157                     pref = new AppPreference(mPrefContext);
158                     pref.setKey(key);
159                     pref.setOnPreferenceClickListener(preference -> {
160                         Bundle args = new Bundle();
161                         args.putString(AppInfoBase.ARG_PACKAGE_NAME, app.info.packageName);
162                         args.putInt(AppInfoBase.ARG_PACKAGE_UID, app.info.uid);
163                         new SubSettingLauncher(mContext)
164                                 .setDestination(AppChannelsBypassingDndSettings.class.getName())
165                                 .setArguments(args)
166                                 .setUserHandle(UserHandle.getUserHandleForUid(app.info.uid))
167                                 .setResultListener(mHostFragment, 0)
168                                 .setSourceMetricsCategory(
169                                         SettingsEnums.NOTIFICATION_ZEN_MODE_OVERRIDING_APP)
170                                 .launch();
171                         return true;
172                     });
173                     pref.setTitle(BidiFormatter.getInstance().unicodeWrap(app.label));
174                     updateIcon(pref, app);
175                     if (appChannels > appChannelsBypassingDnd) {
176                         pref.setSummary(R.string.zen_mode_bypassing_apps_summary_some);
177                     } else {
178                         pref.setSummary(R.string.zen_mode_bypassing_apps_summary_all);
179                     }
180                     mPreferenceCategory.addPreference(pref);
181                 }
182             } else if (appChannelsBypassingDnd == 0) {
183                 // exists but shouldn't anymore
184                 mPreferenceCategory.removePreference(pref);
185             }
186         }
187 
188         Preference pref = mPreferenceCategory.findPreference(KEY_NO_APPS);
189         if (!doAnyAppsPassCriteria) {
190             if (pref == null) {
191                 pref = new Preference(mPrefContext);
192                 pref.setKey(KEY_NO_APPS);
193                 pref.setTitle(R.string.zen_mode_bypassing_apps_none);
194             }
195             mPreferenceCategory.addPreference(pref);
196         } else if (pref != null) {
197             mPreferenceCategory.removePreference(pref);
198         }
199     }
200 
201     /**
202      * Create a unique key to idenfity an AppPreference
203      */
getKey(String pkg, int uid)204     static String getKey(String pkg, int uid) {
205         return "all|" + pkg + "|" + uid;
206     }
207 
208     private final ApplicationsState.Callbacks mAppSessionCallbacks =
209             new ApplicationsState.Callbacks() {
210 
211                 @Override
212                 public void onRunningStateChanged(boolean running) {
213                 }
214 
215                 @Override
216                 public void onPackageListChanged() {
217                 }
218 
219                 @Override
220                 public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) {
221                     updateAppList(apps);
222                 }
223 
224                 @Override
225                 public void onPackageIconChanged() {
226                 }
227 
228                 @Override
229                 public void onPackageSizeChanged(String packageName) {
230                 }
231 
232                 @Override
233                 public void onAllSizesComputed() { }
234 
235                 @Override
236                 public void onLauncherInfoChanged() {
237                 }
238 
239                 @Override
240                 public void onLoadEntriesCompleted() {
241                     updateAppList();
242                 }
243             };
244 }
245