1 /* 2 * Copyright (C) 2016 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.app.NotificationManager.IMPORTANCE_LOW; 20 import static android.app.NotificationManager.IMPORTANCE_NONE; 21 22 import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; 23 24 import android.app.Notification; 25 import android.app.NotificationChannel; 26 import android.app.NotificationChannelGroup; 27 import android.app.NotificationManager; 28 import android.app.role.RoleManager; 29 import android.content.BroadcastReceiver; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.IntentFilter; 33 import android.content.pm.ActivityInfo; 34 import android.content.pm.PackageInfo; 35 import android.content.pm.PackageManager; 36 import android.content.pm.PackageManager.NameNotFoundException; 37 import android.content.pm.ResolveInfo; 38 import android.graphics.BlendMode; 39 import android.graphics.BlendModeColorFilter; 40 import android.graphics.drawable.Drawable; 41 import android.graphics.drawable.GradientDrawable; 42 import android.graphics.drawable.LayerDrawable; 43 import android.os.Bundle; 44 import android.os.UserHandle; 45 import android.provider.Settings; 46 import android.text.TextUtils; 47 import android.util.Log; 48 import android.widget.Toast; 49 50 import androidx.preference.Preference; 51 import androidx.preference.PreferenceGroup; 52 import androidx.preference.PreferenceScreen; 53 54 import com.android.settings.R; 55 import com.android.settings.SettingsActivity; 56 import com.android.settings.Utils; 57 import com.android.settings.applications.AppInfoBase; 58 import com.android.settings.core.SubSettingLauncher; 59 import com.android.settings.dashboard.DashboardFragment; 60 import com.android.settings.widget.MasterSwitchPreference; 61 import com.android.settingslib.RestrictedLockUtilsInternal; 62 63 import java.util.ArrayList; 64 import java.util.Comparator; 65 import java.util.List; 66 67 abstract public class NotificationSettingsBase extends DashboardFragment { 68 private static final String TAG = "NotifiSettingsBase"; 69 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 70 public static final String ARG_FROM_SETTINGS = "fromSettings"; 71 72 protected PackageManager mPm; 73 protected NotificationBackend mBackend = new NotificationBackend(); 74 protected NotificationManager mNm; 75 protected RoleManager mRm; 76 protected Context mContext; 77 78 protected int mUid; 79 protected int mUserId; 80 protected String mPkg; 81 protected PackageInfo mPkgInfo; 82 protected EnforcedAdmin mSuspendedAppsAdmin; 83 protected NotificationChannelGroup mChannelGroup; 84 protected NotificationChannel mChannel; 85 protected NotificationBackend.AppRow mAppRow; 86 87 protected boolean mShowLegacyChannelConfig = false; 88 protected boolean mListeningToPackageRemove; 89 90 protected List<NotificationPreferenceController> mControllers = new ArrayList<>(); 91 protected List<Preference> mDynamicPreferences = new ArrayList<>(); 92 protected ImportanceListener mImportanceListener = new ImportanceListener(); 93 94 protected Intent mIntent; 95 protected Bundle mArgs; 96 97 @Override onAttach(Context context)98 public void onAttach(Context context) { 99 super.onAttach(context); 100 mContext = getActivity(); 101 mIntent = getActivity().getIntent(); 102 mArgs = getArguments(); 103 104 mPm = getPackageManager(); 105 mNm = NotificationManager.from(mContext); 106 mRm = mContext.getSystemService(RoleManager.class); 107 108 mPkg = mArgs != null && mArgs.containsKey(AppInfoBase.ARG_PACKAGE_NAME) 109 ? mArgs.getString(AppInfoBase.ARG_PACKAGE_NAME) 110 : mIntent.getStringExtra(Settings.EXTRA_APP_PACKAGE); 111 mUid = mArgs != null && mArgs.containsKey(AppInfoBase.ARG_PACKAGE_UID) 112 ? mArgs.getInt(AppInfoBase.ARG_PACKAGE_UID) 113 : mIntent.getIntExtra(Settings.EXTRA_APP_UID, -1); 114 115 if (mUid < 0) { 116 try { 117 mUid = mPm.getPackageUid(mPkg, 0); 118 } catch (NameNotFoundException e) { 119 } 120 } 121 122 mPkgInfo = findPackageInfo(mPkg, mUid); 123 124 if (mPkgInfo != null) { 125 mUserId = UserHandle.getUserId(mUid); 126 mSuspendedAppsAdmin = RestrictedLockUtilsInternal.checkIfApplicationIsSuspended( 127 mContext, mPkg, mUserId); 128 129 130 loadChannel(); 131 loadAppRow(); 132 loadChannelGroup(); 133 collectConfigActivities(); 134 135 getSettingsLifecycle().addObserver(use(HeaderPreferenceController.class)); 136 137 for (NotificationPreferenceController controller : mControllers) { 138 controller.onResume(mAppRow, mChannel, mChannelGroup, mSuspendedAppsAdmin); 139 } 140 } 141 } 142 143 @Override onCreate(Bundle savedInstanceState)144 public void onCreate(Bundle savedInstanceState) { 145 super.onCreate(savedInstanceState); 146 147 if (mIntent == null && mArgs == null) { 148 Log.w(TAG, "No intent"); 149 toastAndFinish(); 150 return; 151 } 152 153 if (mUid < 0 || TextUtils.isEmpty(mPkg) || mPkgInfo == null) { 154 Log.w(TAG, "Missing package or uid or packageinfo"); 155 toastAndFinish(); 156 return; 157 } 158 159 startListeningToPackageRemove(); 160 } 161 162 @Override onDestroy()163 public void onDestroy() { 164 stopListeningToPackageRemove(); 165 super.onDestroy(); 166 } 167 168 @Override onResume()169 public void onResume() { 170 super.onResume(); 171 if (mUid < 0 || TextUtils.isEmpty(mPkg) || mPkgInfo == null || mAppRow == null) { 172 Log.w(TAG, "Missing package or uid or packageinfo"); 173 finish(); 174 return; 175 } 176 // Reload app, channel, etc onResume in case they've changed. A little wasteful if we've 177 // just done onAttach but better than making every preference controller reload all 178 // the data 179 loadAppRow(); 180 if (mAppRow == null) { 181 Log.w(TAG, "Can't load package"); 182 finish(); 183 return; 184 } 185 loadChannel(); 186 loadChannelGroup(); 187 collectConfigActivities(); 188 } 189 loadChannel()190 private void loadChannel() { 191 Intent intent = getActivity().getIntent(); 192 String channelId = intent != null ? intent.getStringExtra(Settings.EXTRA_CHANNEL_ID) : null; 193 if (channelId == null && intent != null) { 194 Bundle args = intent.getBundleExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS); 195 channelId = args != null ? args.getString(Settings.EXTRA_CHANNEL_ID) : null; 196 } 197 mChannel = mBackend.getChannel(mPkg, mUid, channelId); 198 } 199 loadAppRow()200 private void loadAppRow() { 201 mAppRow = mBackend.loadAppRow(mContext, mPm, mRm, mPkgInfo); 202 } 203 loadChannelGroup()204 private void loadChannelGroup() { 205 mShowLegacyChannelConfig = mBackend.onlyHasDefaultChannel(mAppRow.pkg, mAppRow.uid) 206 || (mChannel != null 207 && NotificationChannel.DEFAULT_CHANNEL_ID.equals(mChannel.getId())); 208 209 if (mShowLegacyChannelConfig) { 210 mChannel = mBackend.getChannel( 211 mAppRow.pkg, mAppRow.uid, NotificationChannel.DEFAULT_CHANNEL_ID); 212 } 213 if (mChannel != null && !TextUtils.isEmpty(mChannel.getGroup())) { 214 NotificationChannelGroup group = mBackend.getGroup(mPkg, mUid, mChannel.getGroup()); 215 if (group != null) { 216 mChannelGroup = group; 217 } 218 } 219 } 220 toastAndFinish()221 protected void toastAndFinish() { 222 Toast.makeText(mContext, R.string.app_not_found_dlg_text, Toast.LENGTH_SHORT).show(); 223 getActivity().finish(); 224 } 225 collectConfigActivities()226 protected void collectConfigActivities() { 227 Intent intent = new Intent(Intent.ACTION_MAIN) 228 .addCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES) 229 .setPackage(mAppRow.pkg); 230 final List<ResolveInfo> resolveInfos = mPm.queryIntentActivities( 231 intent, 232 0 //PackageManager.MATCH_DEFAULT_ONLY 233 ); 234 if (DEBUG) { 235 Log.d(TAG, "Found " + resolveInfos.size() + " preference activities" 236 + (resolveInfos.size() == 0 ? " ;_;" : "")); 237 } 238 for (ResolveInfo ri : resolveInfos) { 239 final ActivityInfo activityInfo = ri.activityInfo; 240 if (mAppRow.settingsIntent != null) { 241 if (DEBUG) { 242 Log.d(TAG, "Ignoring duplicate notification preference activity (" 243 + activityInfo.name + ") for package " 244 + activityInfo.packageName); 245 } 246 continue; 247 } 248 // TODO(78660939): This should actually start a new task 249 mAppRow.settingsIntent = intent 250 .setPackage(null) 251 .setClassName(activityInfo.packageName, activityInfo.name); 252 if (mChannel != null) { 253 mAppRow.settingsIntent.putExtra(Notification.EXTRA_CHANNEL_ID, mChannel.getId()); 254 } 255 if (mChannelGroup != null) { 256 mAppRow.settingsIntent.putExtra( 257 Notification.EXTRA_CHANNEL_GROUP_ID, mChannelGroup.getId()); 258 } 259 } 260 } 261 findPackageInfo(String pkg, int uid)262 private PackageInfo findPackageInfo(String pkg, int uid) { 263 if (pkg == null || uid < 0) { 264 return null; 265 } 266 final String[] packages = mPm.getPackagesForUid(uid); 267 if (packages != null && pkg != null) { 268 final int N = packages.length; 269 for (int i = 0; i < N; i++) { 270 final String p = packages[i]; 271 if (pkg.equals(p)) { 272 try { 273 return mPm.getPackageInfo(pkg, PackageManager.GET_SIGNATURES); 274 } catch (NameNotFoundException e) { 275 Log.w(TAG, "Failed to load package " + pkg, e); 276 } 277 } 278 } 279 } 280 return null; 281 } 282 getAlertingIcon()283 private Drawable getAlertingIcon() { 284 Drawable icon = getContext().getDrawable(R.drawable.ic_notifications_alert); 285 icon.setTintList(Utils.getColorAccent(getContext())); 286 return icon; 287 } 288 populateSingleChannelPrefs(PreferenceGroup parent, final NotificationChannel channel, final boolean groupBlocked)289 protected Preference populateSingleChannelPrefs(PreferenceGroup parent, 290 final NotificationChannel channel, final boolean groupBlocked) { 291 MasterSwitchPreference channelPref = new MasterSwitchPreference(getPrefContext()); 292 channelPref.setSwitchEnabled(mSuspendedAppsAdmin == null 293 && isChannelBlockable(channel) 294 && isChannelConfigurable(channel) 295 && !groupBlocked); 296 channelPref.setIcon(null); 297 if (channel.getImportance() > IMPORTANCE_LOW) { 298 channelPref.setIcon(getAlertingIcon()); 299 } 300 channelPref.setIconSize(MasterSwitchPreference.ICON_SIZE_SMALL); 301 channelPref.setKey(channel.getId()); 302 channelPref.setTitle(channel.getName()); 303 channelPref.setSummary(NotificationBackend.getSentSummary( 304 mContext, mAppRow.sentByChannel.get(channel.getId()), false)); 305 channelPref.setChecked(channel.getImportance() != IMPORTANCE_NONE); 306 Bundle channelArgs = new Bundle(); 307 channelArgs.putInt(AppInfoBase.ARG_PACKAGE_UID, mUid); 308 channelArgs.putString(AppInfoBase.ARG_PACKAGE_NAME, mPkg); 309 channelArgs.putString(Settings.EXTRA_CHANNEL_ID, channel.getId()); 310 channelArgs.putBoolean(ARG_FROM_SETTINGS, true); 311 channelPref.setIntent(new SubSettingLauncher(getActivity()) 312 .setDestination(ChannelNotificationSettings.class.getName()) 313 .setArguments(channelArgs) 314 .setTitleRes(R.string.notification_channel_title) 315 .setSourceMetricsCategory(getMetricsCategory()) 316 .toIntent()); 317 318 channelPref.setOnPreferenceChangeListener( 319 (preference, o) -> { 320 boolean value = (Boolean) o; 321 int importance = value ? IMPORTANCE_LOW : IMPORTANCE_NONE; 322 channel.setImportance(importance); 323 channel.lockFields( 324 NotificationChannel.USER_LOCKED_IMPORTANCE); 325 MasterSwitchPreference channelPref1 = (MasterSwitchPreference) preference; 326 channelPref1.setIcon(null); 327 if (channel.getImportance() > IMPORTANCE_LOW) { 328 channelPref1.setIcon(getAlertingIcon()); 329 } 330 toggleBehaviorIconState(channelPref1.getIcon(), 331 importance != IMPORTANCE_NONE); 332 mBackend.updateChannel(mPkg, mUid, channel); 333 334 return true; 335 }); 336 if (parent.findPreference(channelPref.getKey()) == null) { 337 parent.addPreference(channelPref); 338 } 339 return channelPref; 340 } 341 toggleBehaviorIconState(Drawable icon, boolean enabled)342 private void toggleBehaviorIconState(Drawable icon, boolean enabled) { 343 if (icon == null) return; 344 345 LayerDrawable layerDrawable = (LayerDrawable) icon; 346 GradientDrawable background = 347 (GradientDrawable) layerDrawable.findDrawableByLayerId(R.id.back); 348 349 if (background == null) return; 350 351 if (enabled) { 352 background.clearColorFilter(); 353 } else { 354 background.setColorFilter(new BlendModeColorFilter( 355 mContext.getColor(R.color.material_grey_300), 356 BlendMode.SRC_IN)); 357 } 358 } 359 isChannelConfigurable(NotificationChannel channel)360 protected boolean isChannelConfigurable(NotificationChannel channel) { 361 if (channel != null && mAppRow != null) { 362 return !channel.isImportanceLockedByOEM(); 363 } 364 return false; 365 } 366 isChannelBlockable(NotificationChannel channel)367 protected boolean isChannelBlockable(NotificationChannel channel) { 368 if (channel != null && mAppRow != null) { 369 if (!mAppRow.systemApp) { 370 return true; 371 } 372 373 if (channel.isImportanceLockedByCriticalDeviceFunction()) { 374 return false; 375 } 376 377 if (channel.isImportanceLockedByOEM()) { 378 return false; 379 } 380 381 return channel.isBlockableSystem() 382 || channel.getImportance() == NotificationManager.IMPORTANCE_NONE; 383 } 384 return false; 385 } 386 isChannelGroupBlockable(NotificationChannelGroup group)387 protected boolean isChannelGroupBlockable(NotificationChannelGroup group) { 388 if (group != null && mAppRow != null) { 389 if (!mAppRow.systemApp) { 390 return true; 391 } 392 393 return group.isBlocked(); 394 } 395 return false; 396 } 397 setVisible(Preference p, boolean visible)398 protected void setVisible(Preference p, boolean visible) { 399 setVisible(getPreferenceScreen(), p, visible); 400 } 401 setVisible(PreferenceGroup parent, Preference p, boolean visible)402 protected void setVisible(PreferenceGroup parent, Preference p, boolean visible) { 403 final boolean isVisible = parent.findPreference(p.getKey()) != null; 404 if (isVisible == visible) return; 405 if (visible) { 406 parent.addPreference(p); 407 } else { 408 parent.removePreference(p); 409 } 410 } 411 startListeningToPackageRemove()412 protected void startListeningToPackageRemove() { 413 if (mListeningToPackageRemove) { 414 return; 415 } 416 mListeningToPackageRemove = true; 417 final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED); 418 filter.addDataScheme("package"); 419 getContext().registerReceiver(mPackageRemovedReceiver, filter); 420 } 421 stopListeningToPackageRemove()422 protected void stopListeningToPackageRemove() { 423 if (!mListeningToPackageRemove) { 424 return; 425 } 426 mListeningToPackageRemove = false; 427 getContext().unregisterReceiver(mPackageRemovedReceiver); 428 } 429 onPackageRemoved()430 protected void onPackageRemoved() { 431 getActivity().finishAndRemoveTask(); 432 } 433 434 protected final BroadcastReceiver mPackageRemovedReceiver = new BroadcastReceiver() { 435 @Override 436 public void onReceive(Context context, Intent intent) { 437 String packageName = intent.getData().getSchemeSpecificPart(); 438 if (mPkgInfo == null || TextUtils.equals(mPkgInfo.packageName, packageName)) { 439 if (DEBUG) { 440 Log.d(TAG, "Package (" + packageName + ") removed. Removing" 441 + "NotificationSettingsBase."); 442 } 443 onPackageRemoved(); 444 } 445 } 446 }; 447 448 protected Comparator<NotificationChannel> mChannelComparator = 449 (left, right) -> { 450 if (left.isDeleted() != right.isDeleted()) { 451 return Boolean.compare(left.isDeleted(), right.isDeleted()); 452 } else if (left.getId().equals(NotificationChannel.DEFAULT_CHANNEL_ID)) { 453 // Uncategorized/miscellaneous legacy channel goes last 454 return 1; 455 } else if (right.getId().equals(NotificationChannel.DEFAULT_CHANNEL_ID)) { 456 return -1; 457 } 458 459 return left.getId().compareTo(right.getId()); 460 }; 461 462 protected class ImportanceListener { onImportanceChanged()463 protected void onImportanceChanged() { 464 final PreferenceScreen screen = getPreferenceScreen(); 465 for (NotificationPreferenceController controller : mControllers) { 466 controller.displayPreference(screen); 467 } 468 updatePreferenceStates(); 469 470 boolean hideDynamicFields = false; 471 if (mAppRow == null || mAppRow.banned) { 472 hideDynamicFields = true; 473 } else { 474 if (mChannel != null) { 475 hideDynamicFields = mChannel.getImportance() == IMPORTANCE_NONE; 476 } else if (mChannelGroup != null) { 477 hideDynamicFields = mChannelGroup.isBlocked(); 478 } 479 } 480 for (Preference preference : mDynamicPreferences) { 481 setVisible(getPreferenceScreen(), preference, !hideDynamicFields); 482 } 483 } 484 } 485 } 486