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