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