1 /* 2 * Copyright (C) 2025 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; 18 19 import static android.service.notification.Adjustment.KEY_SUMMARIZATION; 20 import static android.service.notification.Adjustment.KEY_TYPE; 21 22 import android.app.Flags; 23 import android.content.Context; 24 import android.graphics.drawable.Drawable; 25 import android.service.notification.Adjustment; 26 27 import androidx.annotation.NonNull; 28 import androidx.annotation.Nullable; 29 import androidx.annotation.VisibleForTesting; 30 import androidx.core.text.BidiFormatter; 31 import androidx.fragment.app.Fragment; 32 import androidx.preference.Preference; 33 import androidx.preference.PreferenceCategory; 34 import androidx.preference.PreferenceScreen; 35 36 import com.android.settings.core.BasePreferenceController; 37 import com.android.settings.core.PreferenceControllerMixin; 38 import com.android.settingslib.applications.AppUtils; 39 import com.android.settingslib.applications.ApplicationsState; 40 import com.android.settingslib.utils.ThreadUtils; 41 42 import com.google.common.util.concurrent.ListenableFuture; 43 44 import java.util.ArrayList; 45 import java.util.List; 46 47 /** 48 * Adds a preference to the PreferenceCategory for every app excluded from an adjustment key 49 */ 50 public class AdjustmentExcludedAppsPreferenceController extends BasePreferenceController 51 implements PreferenceControllerMixin { 52 53 @NonNull private NotificationBackend mBackend; 54 55 @Nullable String mAdjustmentKey; 56 @Nullable @VisibleForTesting ApplicationsState mApplicationsState; 57 @VisibleForTesting PreferenceCategory mPreferenceCategory; 58 @VisibleForTesting Context mPrefContext; 59 60 private ApplicationsState.Session mAppSession; 61 AdjustmentExcludedAppsPreferenceController(@onNull Context context, @NonNull String preferenceKey)62 public AdjustmentExcludedAppsPreferenceController(@NonNull Context context, 63 @NonNull String preferenceKey) { 64 super(context, preferenceKey); 65 mBackend = new NotificationBackend(); 66 } 67 onAttach(@ullable ApplicationsState appState, @Nullable Fragment host, @NonNull NotificationBackend helperBackend, @Adjustment.Keys String adjustment)68 protected void onAttach(@Nullable ApplicationsState appState, @Nullable Fragment host, 69 @NonNull NotificationBackend helperBackend, @Adjustment.Keys String adjustment) { 70 mApplicationsState = appState; 71 mBackend = helperBackend; 72 mAdjustmentKey = adjustment; 73 74 if (mApplicationsState != null && host != null) { 75 mAppSession = mApplicationsState.newSession(mAppSessionCallbacks, host.getLifecycle()); 76 } 77 } 78 79 @Override displayPreference(@onNull PreferenceScreen screen)80 public void displayPreference(@NonNull PreferenceScreen screen) { 81 mPreferenceCategory = screen.findPreference(getPreferenceKey()); 82 mPrefContext = screen.getContext(); 83 updateAppList(); 84 super.displayPreference(screen); 85 } 86 87 @Override getAvailabilityStatus()88 public int getAvailabilityStatus() { 89 if (!(Flags.nmSummarization() || Flags.nmSummarizationUi() 90 || Flags.notificationClassificationUi())) { 91 return CONDITIONALLY_UNAVAILABLE; 92 } 93 if (KEY_SUMMARIZATION.equals(mAdjustmentKey) 94 && mBackend.isNotificationSummarizationSupported()) { 95 return AVAILABLE; 96 } 97 if (KEY_TYPE.equals(mAdjustmentKey) && mBackend.isNotificationBundlingSupported()) { 98 return AVAILABLE; 99 } 100 return CONDITIONALLY_UNAVAILABLE; 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 ListenableFuture unused = 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 List<String> excludedApps = List.of(mBackend.getAdjustmentDeniedPackages(mAdjustmentKey)); 143 144 for (ApplicationsState.AppEntry app : apps) { 145 String pkg = app.info.packageName; 146 final String key = getKey(pkg, app.info.uid); 147 boolean doesAppPassCriteria = false; 148 149 if (excludedApps.contains(pkg)) { 150 doesAppPassCriteria = true; 151 } 152 Preference pref = mPreferenceCategory.findPreference(key); 153 if (pref == null) { 154 if (doesAppPassCriteria) { 155 // does not exist but should 156 pref = new Preference(mPrefContext); 157 pref.setKey(key); 158 pref.setTitle(BidiFormatter.getInstance().unicodeWrap(app.label)); 159 updateIcon(pref, app); 160 mPreferenceCategory.addPreference(pref); 161 } 162 } else if (!doesAppPassCriteria) { 163 // exists but shouldn't anymore 164 mPreferenceCategory.removePreference(pref); 165 } 166 } 167 } 168 169 /** 170 * Create a unique key to identify an AppPreference 171 */ getKey(String pkg, int uid)172 static String getKey(String pkg, int uid) { 173 return "all|" + pkg + "|" + uid; 174 } 175 176 private final ApplicationsState.Callbacks mAppSessionCallbacks = 177 new ApplicationsState.Callbacks() { 178 179 @Override 180 public void onRunningStateChanged(boolean running) { 181 } 182 183 @Override 184 public void onPackageListChanged() { 185 } 186 187 @Override 188 public void onRebuildComplete(@NonNull ArrayList<ApplicationsState.AppEntry> apps) { 189 updateAppList(apps); 190 } 191 192 @Override 193 public void onPackageIconChanged() { 194 } 195 196 @Override 197 public void onPackageSizeChanged(@NonNull String packageName) { 198 } 199 200 @Override 201 public void onAllSizesComputed() { } 202 203 @Override 204 public void onLauncherInfoChanged() { 205 } 206 207 @Override 208 public void onLoadEntriesCompleted() { 209 updateAppList(); 210 } 211 }; 212 } 213