1 /*
2  * Copyright (C) 2018 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.os.Bundle;
23 import android.os.UserHandle;
24 
25 import androidx.annotation.VisibleForTesting;
26 import androidx.core.text.BidiFormatter;
27 import androidx.fragment.app.Fragment;
28 import androidx.preference.Preference;
29 import androidx.preference.PreferenceCategory;
30 import androidx.preference.PreferenceScreen;
31 
32 import com.android.settings.R;
33 import com.android.settings.applications.AppInfoBase;
34 import com.android.settings.core.PreferenceControllerMixin;
35 import com.android.settings.core.SubSettingLauncher;
36 import com.android.settings.notification.NotificationBackend;
37 import com.android.settings.notification.app.AppChannelsBypassingDndSettings;
38 import com.android.settingslib.applications.ApplicationsState;
39 import com.android.settingslib.core.AbstractPreferenceController;
40 import com.android.settingslib.widget.apppreference.AppPreference;
41 
42 import java.util.ArrayList;
43 import java.util.List;
44 import java.util.Objects;
45 
46 
47 /**
48  * Adds a preference to the PreferenceScreen for each notification channel that can bypass DND.
49  */
50 public class ZenModeAllBypassingAppsPreferenceController extends AbstractPreferenceController
51         implements PreferenceControllerMixin {
52     public static final String KEY_NO_APPS = getKey("none");
53     private static final String KEY = "zen_mode_bypassing_apps_list";
54 
55     private final NotificationBackend mNotificationBackend;
56 
57     @VisibleForTesting ApplicationsState mApplicationsState;
58     @VisibleForTesting PreferenceCategory mPreferenceCategory;
59     @VisibleForTesting Context mPrefContext;
60 
61     private ApplicationsState.Session mAppSession;
62     private Fragment mHostFragment;
63 
ZenModeAllBypassingAppsPreferenceController(Context context, Application app, Fragment host, NotificationBackend notificationBackend)64     public ZenModeAllBypassingAppsPreferenceController(Context context, Application app,
65             Fragment host, NotificationBackend notificationBackend) {
66         this(context, app == null ? null : ApplicationsState.getInstance(app), host,
67                 notificationBackend);
68     }
69 
ZenModeAllBypassingAppsPreferenceController(Context context, ApplicationsState appState, Fragment host, NotificationBackend notificationBackend)70     private ZenModeAllBypassingAppsPreferenceController(Context context, ApplicationsState appState,
71             Fragment host, NotificationBackend notificationBackend) {
72         super(context);
73         mNotificationBackend = notificationBackend;
74         mApplicationsState = appState;
75         mHostFragment = host;
76 
77         if (mApplicationsState != null && host != null) {
78             mAppSession = mApplicationsState.newSession(mAppSessionCallbacks, host.getLifecycle());
79         }
80     }
81 
82     @Override
displayPreference(PreferenceScreen screen)83     public void displayPreference(PreferenceScreen screen) {
84         mPreferenceCategory = screen.findPreference(KEY);
85         mPrefContext = screen.getContext();
86         updateAppList();
87         super.displayPreference(screen);
88     }
89 
90     @Override
isAvailable()91     public boolean isAvailable() {
92         return true;
93     }
94 
95     @Override
getPreferenceKey()96     public String getPreferenceKey() {
97         return KEY;
98     }
99 
100     /**
101      * Call this method to trigger the app list to refresh.
102      */
updateAppList()103     public void updateAppList() {
104         if (mAppSession == null) {
105             return;
106         }
107 
108         ApplicationsState.AppFilter filter = ApplicationsState.FILTER_ALL_ENABLED;
109         List<ApplicationsState.AppEntry> apps = mAppSession.rebuild(filter,
110                 ApplicationsState.ALPHA_COMPARATOR);
111         updateAppList(apps);
112     }
113 
114     @VisibleForTesting
updateAppList(List<ApplicationsState.AppEntry> apps)115     void updateAppList(List<ApplicationsState.AppEntry> apps) {
116         if (mPreferenceCategory == null || apps == null) {
117             return;
118         }
119 
120         List<Preference> appsBypassingDnd = new ArrayList<>();
121         for (ApplicationsState.AppEntry app : apps) {
122             String pkg = app.info.packageName;
123             mApplicationsState.ensureIcon(app);
124             final int appChannels = mNotificationBackend.getChannelCount(pkg, app.info.uid);
125             final int appChannelsBypassingDnd = mNotificationBackend
126                     .getNotificationChannelsBypassingDnd(pkg, app.info.uid).getList().size();
127             if (appChannelsBypassingDnd > 0) {
128                 final String key = getKey(pkg);
129                 // re-use previously created preference when possible
130                 Preference pref = mPreferenceCategory.findPreference(key);
131                 if (pref == null) {
132                     pref = new AppPreference(mPrefContext);
133                     pref.setKey(key);
134                     pref.setOnPreferenceClickListener(preference -> {
135                         Bundle args = new Bundle();
136                         args.putString(AppInfoBase.ARG_PACKAGE_NAME, app.info.packageName);
137                         args.putInt(AppInfoBase.ARG_PACKAGE_UID, app.info.uid);
138                         new SubSettingLauncher(mContext)
139                                 .setDestination(AppChannelsBypassingDndSettings.class.getName())
140                                 .setArguments(args)
141                                 .setUserHandle(UserHandle.getUserHandleForUid(app.info.uid))
142                                 .setResultListener(mHostFragment, 0)
143                                 .setSourceMetricsCategory(
144                                         SettingsEnums.NOTIFICATION_ZEN_MODE_OVERRIDING_APP)
145                                 .launch();
146                         return true;
147                     });
148                 }
149                 pref.setTitle(BidiFormatter.getInstance().unicodeWrap(app.label));
150                 pref.setIcon(app.icon);
151                 if (appChannels > appChannelsBypassingDnd) {
152                     pref.setSummary(R.string.zen_mode_bypassing_apps_summary_some);
153                 } else {
154                     pref.setSummary(R.string.zen_mode_bypassing_apps_summary_all);
155                 }
156 
157                 appsBypassingDnd.add(pref);
158             }
159         }
160 
161         if (appsBypassingDnd.size() == 0) {
162             Preference pref = mPreferenceCategory.findPreference(KEY_NO_APPS);
163             if (pref == null) {
164                 pref = new Preference(mPrefContext);
165                 pref.setKey(KEY_NO_APPS);
166                 pref.setTitle(R.string.zen_mode_bypassing_apps_none);
167             }
168             appsBypassingDnd.add(pref);
169         }
170 
171         if (hasAppListChanged(appsBypassingDnd, mPreferenceCategory)) {
172             mPreferenceCategory.removeAll();
173             for (Preference prefToAdd : appsBypassingDnd) {
174                 mPreferenceCategory.addPreference(prefToAdd);
175             }
176         }
177     }
178 
hasAppListChanged(List<Preference> newAppPreferences, PreferenceCategory preferenceCategory)179     static boolean hasAppListChanged(List<Preference> newAppPreferences,
180             PreferenceCategory preferenceCategory) {
181         if (newAppPreferences.size() != preferenceCategory.getPreferenceCount()) {
182             return true;
183         }
184 
185         for (int i = 0; i < newAppPreferences.size(); i++) {
186             Preference newAppPref = newAppPreferences.get(i);
187             Preference pref = preferenceCategory.getPreference(i);
188             if (!Objects.equals(newAppPref.getKey(), pref.getKey())) {
189                 return true;
190             }
191         }
192         return false;
193 
194     }
195 
196     /**
197      * Create a unique key to idenfity an AppPreference
198      */
getKey(String pkg)199     static String getKey(String pkg) {
200         return pkg;
201     }
202 
203     private final ApplicationsState.Callbacks mAppSessionCallbacks =
204             new ApplicationsState.Callbacks() {
205 
206                 @Override
207                 public void onRunningStateChanged(boolean running) {
208                     updateAppList();
209                 }
210 
211                 @Override
212                 public void onPackageListChanged() {
213                     updateAppList();
214                 }
215 
216                 @Override
217                 public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) {
218                     updateAppList(apps);
219                 }
220 
221                 @Override
222                 public void onPackageIconChanged() {
223                     updateAppList();
224                 }
225 
226                 @Override
227                 public void onPackageSizeChanged(String packageName) {
228                     updateAppList();
229                 }
230 
231                 @Override
232                 public void onAllSizesComputed() { }
233 
234                 @Override
235                 public void onLauncherInfoChanged() {
236                     updateAppList();
237                 }
238 
239                 @Override
240                 public void onLoadEntriesCompleted() {
241                     updateAppList();
242                 }
243             };
244 }
245