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