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