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.app; 18 19 import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; 20 21 import android.animation.AnimatorSet; 22 import android.animation.ObjectAnimator; 23 import android.animation.ValueAnimator; 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.content.pm.ShortcutInfo; 38 import android.graphics.drawable.Drawable; 39 import android.os.Bundle; 40 import android.os.UserHandle; 41 import android.provider.Settings; 42 import android.text.TextUtils; 43 import android.util.Log; 44 import android.view.View; 45 import android.view.ViewGroup; 46 import android.view.ViewTreeObserver; 47 import android.view.animation.DecelerateInterpolator; 48 import android.widget.Toast; 49 50 import androidx.annotation.NonNull; 51 import androidx.preference.PreferenceScreen; 52 53 import com.android.settings.R; 54 import com.android.settings.SettingsActivity; 55 import com.android.settings.applications.AppInfoBase; 56 import com.android.settings.dashboard.DashboardFragment; 57 import com.android.settings.notification.NotificationBackend; 58 import com.android.settingslib.RestrictedLockUtilsInternal; 59 import com.android.settingslib.notification.ConversationIconFactory; 60 61 import java.util.ArrayList; 62 import java.util.List; 63 64 abstract public class NotificationSettings extends DashboardFragment { 65 private static final String TAG = "NotifiSettingsBase"; 66 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 67 68 protected PackageManager mPm; 69 protected NotificationBackend mBackend = new NotificationBackend(); 70 protected NotificationManager mNm; 71 protected Context mContext; 72 73 protected int mUid; 74 protected int mUserId; 75 protected String mPkg; 76 protected PackageInfo mPkgInfo; 77 protected EnforcedAdmin mSuspendedAppsAdmin; 78 protected NotificationChannelGroup mChannelGroup; 79 protected NotificationChannel mChannel; 80 protected NotificationBackend.AppRow mAppRow; 81 protected Drawable mConversationDrawable; 82 protected ShortcutInfo mConversationInfo; 83 protected List<String> mPreferenceFilter; 84 85 protected boolean mShowLegacyChannelConfig = false; 86 protected boolean mListeningToPackageRemove; 87 88 protected List<NotificationPreferenceController> mControllers = new ArrayList<>(); 89 protected DependentFieldListener mDependentFieldListener = new DependentFieldListener(); 90 91 protected Intent mIntent; 92 protected Bundle mArgs; 93 94 private ViewGroup mLayoutView; 95 private static final int DURATION_ANIMATE_PANEL_EXPAND_MS = 250; 96 97 private final ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener = 98 new ViewTreeObserver.OnGlobalLayoutListener() { 99 @Override 100 public void onGlobalLayout() { 101 animateIn(); 102 if (mLayoutView != null) { 103 mLayoutView.getViewTreeObserver().removeOnGlobalLayoutListener(this); 104 } 105 } 106 }; 107 108 @Override onAttach(Context context)109 public void onAttach(Context context) { 110 super.onAttach(context); 111 mContext = getActivity(); 112 mIntent = getActivity().getIntent(); 113 mArgs = getArguments(); 114 115 mPm = getPackageManager(); 116 mNm = NotificationManager.from(mContext); 117 118 mPkg = mArgs != null && mArgs.containsKey(AppInfoBase.ARG_PACKAGE_NAME) 119 ? mArgs.getString(AppInfoBase.ARG_PACKAGE_NAME) 120 : mIntent.getStringExtra(Settings.EXTRA_APP_PACKAGE); 121 mUid = mArgs != null && mArgs.containsKey(AppInfoBase.ARG_PACKAGE_UID) 122 ? mArgs.getInt(AppInfoBase.ARG_PACKAGE_UID) 123 : mIntent.getIntExtra(Settings.EXTRA_APP_UID, -1); 124 125 if (mUid < 0) { 126 try { 127 mUid = mPm.getPackageUid(mPkg, 0); 128 } catch (NameNotFoundException e) { 129 } 130 } 131 132 mPkgInfo = findPackageInfo(mPkg, mUid); 133 134 if (mPkgInfo != null) { 135 mUserId = UserHandle.getUserId(mUid); 136 mSuspendedAppsAdmin = RestrictedLockUtilsInternal.checkIfApplicationIsSuspended( 137 mContext, mPkg, mUserId); 138 139 loadChannel(); 140 loadAppRow(); 141 loadChannelGroup(); 142 loadPreferencesFilter(); 143 collectConfigActivities(); 144 145 if (use(HeaderPreferenceController.class) != null) { 146 getSettingsLifecycle().addObserver(use(HeaderPreferenceController.class)); 147 } 148 if (use(ConversationHeaderPreferenceController.class) != null) { 149 getSettingsLifecycle().addObserver( 150 use(ConversationHeaderPreferenceController.class)); 151 } 152 153 for (NotificationPreferenceController controller : mControllers) { 154 controller.onResume(mAppRow, mChannel, mChannelGroup, null, null, 155 mSuspendedAppsAdmin, mPreferenceFilter); 156 } 157 } 158 } 159 160 @Override onCreate(Bundle savedInstanceState)161 public void onCreate(Bundle savedInstanceState) { 162 super.onCreate(savedInstanceState); 163 164 if (mIntent == null && mArgs == null) { 165 Log.w(TAG, "No intent"); 166 toastAndFinish(); 167 return; 168 } 169 170 if (mUid < 0 || TextUtils.isEmpty(mPkg) || mPkgInfo == null) { 171 Log.w(TAG, "Missing package or uid or packageinfo"); 172 toastAndFinish(); 173 return; 174 } 175 176 startListeningToPackageRemove(); 177 } 178 179 @Override onDestroy()180 public void onDestroy() { 181 stopListeningToPackageRemove(); 182 super.onDestroy(); 183 } 184 185 @Override onResume()186 public void onResume() { 187 super.onResume(); 188 if (mUid < 0 || TextUtils.isEmpty(mPkg) || mPkgInfo == null || mAppRow == null) { 189 Log.w(TAG, "Missing package or uid or packageinfo"); 190 finish(); 191 return; 192 } 193 // Reload app, channel, etc onResume in case they've changed. A little wasteful if we've 194 // just done onAttach but better than making every preference controller reload all 195 // the data 196 loadAppRow(); 197 if (mAppRow == null) { 198 Log.w(TAG, "Can't load package"); 199 finish(); 200 return; 201 } 202 loadChannel(); 203 loadConversation(); 204 loadChannelGroup(); 205 loadPreferencesFilter(); 206 collectConfigActivities(); 207 } 208 animatePanel()209 protected void animatePanel() { 210 if (mPreferenceFilter != null) { 211 mLayoutView = getActivity().findViewById(R.id.main_content); 212 mLayoutView.getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener); 213 } 214 } 215 216 /** 217 * Animate a Panel onto the screen. 218 * <p> 219 * Takes the entire panel and animates in from behind the navigation bar. 220 * <p> 221 * Relies on the Panel being having a fixed height to begin the animation. 222 */ animateIn()223 private void animateIn() { 224 final AnimatorSet animatorSet = buildAnimatorSet(mLayoutView, 225 mLayoutView.getHeight() /* startY */, 0.0f /* endY */, 226 0.0f /* startAlpha */, 1.0f /* endAlpha */, 227 DURATION_ANIMATE_PANEL_EXPAND_MS); 228 final ValueAnimator animator = new ValueAnimator(); 229 animator.setFloatValues(0.0f, 1.0f); 230 animatorSet.play(animator); 231 animatorSet.start(); 232 } 233 234 /** 235 * Build an {@link AnimatorSet} to animate the Panel, {@param parentView} in or out of the 236 * screen, based on the positional parameters {@param startY}, {@param endY}, the parameters 237 * for alpha changes {@param startAlpha}, {@param endAlpha}, and the {@param duration} in 238 * milliseconds. 239 */ 240 @NonNull buildAnimatorSet(@onNull View targetView, float startY, float endY, float startAlpha, float endAlpha, int duration)241 private static AnimatorSet buildAnimatorSet(@NonNull View targetView, 242 float startY, float endY, 243 float startAlpha, float endAlpha, int duration) { 244 final AnimatorSet animatorSet = new AnimatorSet(); 245 animatorSet.setDuration(duration); 246 animatorSet.setInterpolator(new DecelerateInterpolator()); 247 animatorSet.playTogether( 248 ObjectAnimator.ofFloat(targetView, View.TRANSLATION_Y, startY, endY), 249 ObjectAnimator.ofFloat(targetView, View.ALPHA, startAlpha, endAlpha)); 250 return animatorSet; 251 } 252 loadPreferencesFilter()253 private void loadPreferencesFilter() { 254 Intent intent = getActivity().getIntent(); 255 mPreferenceFilter = intent != null 256 ? intent.getStringArrayListExtra(Settings.EXTRA_CHANNEL_FILTER_LIST) 257 : null; 258 } 259 loadChannel()260 private void loadChannel() { 261 Intent intent = getActivity().getIntent(); 262 String channelId = intent != null ? intent.getStringExtra(Settings.EXTRA_CHANNEL_ID) : null; 263 if (channelId == null && intent != null) { 264 Bundle args = intent.getBundleExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS); 265 channelId = args != null ? args.getString(Settings.EXTRA_CHANNEL_ID) : null; 266 } 267 String conversationId = intent != null 268 ? intent.getStringExtra(Settings.EXTRA_CONVERSATION_ID) : null; 269 mChannel = mBackend.getChannel(mPkg, mUid, channelId, conversationId); 270 if (mChannel == null) { 271 mBackend.getChannel(mPkg, mUid, channelId, null); 272 } 273 } 274 loadConversation()275 private void loadConversation() { 276 if (mChannel == null || TextUtils.isEmpty(mChannel.getConversationId()) 277 || mChannel.isDemoted()) { 278 return; 279 } 280 mConversationInfo = mBackend.getConversationInfo( 281 mContext, mPkg, mUid, mChannel.getConversationId()); 282 if (mConversationInfo != null) { 283 mConversationDrawable = mBackend.getConversationDrawable( 284 mContext, mConversationInfo, mAppRow.pkg, mAppRow.uid, 285 mChannel.isImportantConversation()); 286 } 287 } 288 loadAppRow()289 private void loadAppRow() { 290 mAppRow = mBackend.loadAppRow(mContext, mPm, mPkgInfo); 291 } 292 loadChannelGroup()293 private void loadChannelGroup() { 294 mShowLegacyChannelConfig = mBackend.onlyHasDefaultChannel(mAppRow.pkg, mAppRow.uid) 295 || (mChannel != null 296 && NotificationChannel.DEFAULT_CHANNEL_ID.equals(mChannel.getId())); 297 298 if (mShowLegacyChannelConfig) { 299 mChannel = mBackend.getChannel( 300 mAppRow.pkg, mAppRow.uid, NotificationChannel.DEFAULT_CHANNEL_ID, null); 301 } 302 if (mChannel != null && !TextUtils.isEmpty(mChannel.getGroup())) { 303 NotificationChannelGroup group = mBackend.getGroup(mPkg, mUid, mChannel.getGroup()); 304 if (group != null) { 305 mChannelGroup = group; 306 } 307 } 308 } 309 toastAndFinish()310 protected void toastAndFinish() { 311 Toast.makeText(mContext, R.string.app_not_found_dlg_text, Toast.LENGTH_SHORT).show(); 312 getActivity().finish(); 313 } 314 collectConfigActivities()315 protected void collectConfigActivities() { 316 Intent intent = new Intent(Intent.ACTION_MAIN) 317 .addCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES) 318 .setPackage(mAppRow.pkg); 319 final List<ResolveInfo> resolveInfos = mPm.queryIntentActivities( 320 intent, 321 0 //PackageManager.MATCH_DEFAULT_ONLY 322 ); 323 if (DEBUG) { 324 Log.d(TAG, "Found " + resolveInfos.size() + " preference activities" 325 + (resolveInfos.size() == 0 ? " ;_;" : "")); 326 } 327 for (ResolveInfo ri : resolveInfos) { 328 final ActivityInfo activityInfo = ri.activityInfo; 329 if (mAppRow.settingsIntent != null) { 330 if (DEBUG) { 331 Log.d(TAG, "Ignoring duplicate notification preference activity (" 332 + activityInfo.name + ") for package " 333 + activityInfo.packageName); 334 } 335 continue; 336 } 337 mAppRow.settingsIntent = intent 338 .setPackage(null) 339 .setClassName(activityInfo.packageName, activityInfo.name) 340 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 341 if (mChannel != null) { 342 mAppRow.settingsIntent.putExtra(Notification.EXTRA_CHANNEL_ID, mChannel.getId()); 343 } 344 if (mChannelGroup != null) { 345 mAppRow.settingsIntent.putExtra( 346 Notification.EXTRA_CHANNEL_GROUP_ID, mChannelGroup.getId()); 347 } 348 } 349 } 350 findPackageInfo(String pkg, int uid)351 private PackageInfo findPackageInfo(String pkg, int uid) { 352 if (pkg == null || uid < 0) { 353 return null; 354 } 355 final String[] packages = mPm.getPackagesForUid(uid); 356 if (packages != null && pkg != null) { 357 final int N = packages.length; 358 for (int i = 0; i < N; i++) { 359 final String p = packages[i]; 360 if (pkg.equals(p)) { 361 try { 362 return mPm.getPackageInfo(pkg, PackageManager.GET_SIGNATURES 363 | PackageManager.GET_PERMISSIONS); 364 } catch (NameNotFoundException e) { 365 Log.w(TAG, "Failed to load package " + pkg, e); 366 } 367 } 368 } 369 } 370 return null; 371 } 372 startListeningToPackageRemove()373 protected void startListeningToPackageRemove() { 374 if (mListeningToPackageRemove) { 375 return; 376 } 377 mListeningToPackageRemove = true; 378 final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED); 379 filter.addDataScheme("package"); 380 getContext().registerReceiver(mPackageRemovedReceiver, filter); 381 } 382 stopListeningToPackageRemove()383 protected void stopListeningToPackageRemove() { 384 if (!mListeningToPackageRemove) { 385 return; 386 } 387 mListeningToPackageRemove = false; 388 getContext().unregisterReceiver(mPackageRemovedReceiver); 389 } 390 onPackageRemoved()391 protected void onPackageRemoved() { 392 getActivity().finishAndRemoveTask(); 393 } 394 395 protected final BroadcastReceiver mPackageRemovedReceiver = new BroadcastReceiver() { 396 @Override 397 public void onReceive(Context context, Intent intent) { 398 String packageName = intent.getData().getSchemeSpecificPart(); 399 if (mPkgInfo == null || TextUtils.equals(mPkgInfo.packageName, packageName)) { 400 if (DEBUG) { 401 Log.d(TAG, "Package (" + packageName + ") removed. Removing" 402 + "NotificationSettingsBase."); 403 } 404 onPackageRemoved(); 405 } 406 } 407 }; 408 409 protected class DependentFieldListener { onFieldValueChanged()410 protected void onFieldValueChanged() { 411 // Reload the conversation drawable, which shows some channel/conversation state 412 if (mConversationDrawable != null && mConversationDrawable 413 instanceof ConversationIconFactory.ConversationIconDrawable) { 414 ((ConversationIconFactory.ConversationIconDrawable) mConversationDrawable) 415 .setImportant(mChannel.isImportantConversation()); 416 } 417 final PreferenceScreen screen = getPreferenceScreen(); 418 for (NotificationPreferenceController controller : mControllers) { 419 controller.displayPreference(screen); 420 } 421 updatePreferenceStates(); 422 } 423 } 424 } 425