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