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