1 /* 2 * Copyright (C) 2017 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.systemui.statusbar; 18 19 import static android.app.NotificationManager.IMPORTANCE_NONE; 20 21 import android.app.INotificationManager; 22 import android.app.Notification; 23 import android.app.NotificationChannel; 24 import android.app.NotificationChannelGroup; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.pm.ActivityInfo; 28 import android.content.pm.ApplicationInfo; 29 import android.content.pm.PackageInfo; 30 import android.content.pm.PackageManager; 31 import android.content.pm.ResolveInfo; 32 import android.graphics.drawable.Drawable; 33 import android.os.RemoteException; 34 import android.service.notification.StatusBarNotification; 35 import android.text.TextUtils; 36 import android.util.AttributeSet; 37 import android.view.View; 38 import android.view.accessibility.AccessibilityEvent; 39 import android.widget.ImageView; 40 import android.widget.LinearLayout; 41 import android.widget.Switch; 42 import android.widget.TextView; 43 44 import com.android.internal.logging.MetricsLogger; 45 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 46 import com.android.settingslib.Utils; 47 import com.android.systemui.R; 48 49 import java.lang.IllegalArgumentException; 50 import java.util.List; 51 import java.util.Set; 52 53 /** 54 * The guts of a notification revealed when performing a long press. 55 */ 56 public class NotificationInfo extends LinearLayout implements NotificationGuts.GutsContent { 57 private static final String TAG = "InfoGuts"; 58 59 private INotificationManager mINotificationManager; 60 private String mPkg; 61 private String mAppName; 62 private int mAppUid; 63 private List<NotificationChannel> mNotificationChannels; 64 private NotificationChannel mSingleNotificationChannel; 65 private boolean mIsSingleDefaultChannel; 66 private StatusBarNotification mSbn; 67 private int mStartingUserImportance; 68 69 private TextView mNumChannelsView; 70 private View mChannelDisabledView; 71 private TextView mSettingsLinkView; 72 private Switch mChannelEnabledSwitch; 73 private CheckSaveListener mCheckSaveListener; 74 private OnAppSettingsClickListener mAppSettingsClickListener; 75 private PackageManager mPm; 76 77 private NotificationGuts mGutsContainer; 78 NotificationInfo(Context context, AttributeSet attrs)79 public NotificationInfo(Context context, AttributeSet attrs) { 80 super(context, attrs); 81 } 82 83 // Specify a CheckSaveListener to override when/if the user's changes are committed. 84 public interface CheckSaveListener { 85 // Invoked when importance has changed and the NotificationInfo wants to try to save it. 86 // Listener should run saveImportance unless the change should be canceled. checkSave(Runnable saveImportance)87 void checkSave(Runnable saveImportance); 88 } 89 90 public interface OnSettingsClickListener { onClick(View v, NotificationChannel channel, int appUid)91 void onClick(View v, NotificationChannel channel, int appUid); 92 } 93 94 public interface OnAppSettingsClickListener { onClick(View v, Intent intent)95 void onClick(View v, Intent intent); 96 } 97 bindNotification(final PackageManager pm, final INotificationManager iNotificationManager, final String pkg, final List<NotificationChannel> notificationChannels, int startingUserImportance, final StatusBarNotification sbn, OnSettingsClickListener onSettingsClick, OnAppSettingsClickListener onAppSettingsClick, OnClickListener onDoneClick, CheckSaveListener checkSaveListener, final Set<String> nonBlockablePkgs)98 public void bindNotification(final PackageManager pm, 99 final INotificationManager iNotificationManager, 100 final String pkg, 101 final List<NotificationChannel> notificationChannels, 102 int startingUserImportance, 103 final StatusBarNotification sbn, 104 OnSettingsClickListener onSettingsClick, 105 OnAppSettingsClickListener onAppSettingsClick, 106 OnClickListener onDoneClick, 107 CheckSaveListener checkSaveListener, 108 final Set<String> nonBlockablePkgs) 109 throws RemoteException { 110 mINotificationManager = iNotificationManager; 111 mPkg = pkg; 112 mNotificationChannels = notificationChannels; 113 mCheckSaveListener = checkSaveListener; 114 mSbn = sbn; 115 mPm = pm; 116 mAppSettingsClickListener = onAppSettingsClick; 117 mStartingUserImportance = startingUserImportance; 118 mAppName = mPkg; 119 Drawable pkgicon = null; 120 CharSequence channelNameText = ""; 121 ApplicationInfo info = null; 122 try { 123 info = pm.getApplicationInfo(mPkg, 124 PackageManager.MATCH_UNINSTALLED_PACKAGES 125 | PackageManager.MATCH_DISABLED_COMPONENTS 126 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE 127 | PackageManager.MATCH_DIRECT_BOOT_AWARE); 128 if (info != null) { 129 mAppUid = info.uid; 130 mAppName = String.valueOf(pm.getApplicationLabel(info)); 131 pkgicon = pm.getApplicationIcon(info); 132 } 133 } catch (PackageManager.NameNotFoundException e) { 134 // app is gone, just show package name and generic icon 135 pkgicon = pm.getDefaultActivityIcon(); 136 } 137 ((ImageView) findViewById(R.id.pkgicon)).setImageDrawable(pkgicon); 138 139 int numTotalChannels = iNotificationManager.getNumNotificationChannelsForPackage( 140 pkg, mAppUid, false /* includeDeleted */); 141 if (mNotificationChannels.isEmpty()) { 142 throw new IllegalArgumentException("bindNotification requires at least one channel"); 143 } else { 144 if (mNotificationChannels.size() == 1) { 145 mSingleNotificationChannel = mNotificationChannels.get(0); 146 // Special behavior for the Default channel if no other channels have been defined. 147 mIsSingleDefaultChannel = 148 (mSingleNotificationChannel.getId() 149 .equals(NotificationChannel.DEFAULT_CHANNEL_ID) && 150 numTotalChannels <= 1); 151 } else { 152 mSingleNotificationChannel = null; 153 mIsSingleDefaultChannel = false; 154 } 155 } 156 157 String channelsDescText; 158 mNumChannelsView = findViewById(R.id.num_channels_desc); 159 if (mIsSingleDefaultChannel) { 160 channelsDescText = mContext.getString(R.string.notification_default_channel_desc); 161 } else { 162 switch (mNotificationChannels.size()) { 163 case 1: 164 channelsDescText = String.format(mContext.getResources().getQuantityString( 165 R.plurals.notification_num_channels_desc, numTotalChannels), 166 numTotalChannels); 167 break; 168 case 2: 169 channelsDescText = mContext.getString( 170 R.string.notification_channels_list_desc_2, 171 mNotificationChannels.get(0).getName(), 172 mNotificationChannels.get(1).getName()); 173 break; 174 default: 175 final int numOthers = mNotificationChannels.size() - 2; 176 channelsDescText = String.format( 177 mContext.getResources().getQuantityString( 178 R.plurals.notification_channels_list_desc_2_and_others, 179 numOthers), 180 mNotificationChannels.get(0).getName(), 181 mNotificationChannels.get(1).getName(), 182 numOthers); 183 } 184 } 185 mNumChannelsView.setText(channelsDescText); 186 187 if (mSingleNotificationChannel == null) { 188 // Multiple channels don't use a channel name for the title. 189 channelNameText = mContext.getString(R.string.notification_num_channels, 190 mNotificationChannels.size()); 191 } else if (mIsSingleDefaultChannel) { 192 // If this is the default channel, don't use our channel-specific text. 193 channelNameText = mContext.getString(R.string.notification_header_default_channel); 194 } else { 195 channelNameText = mSingleNotificationChannel.getName(); 196 } 197 ((TextView) findViewById(R.id.pkgname)).setText(mAppName); 198 ((TextView) findViewById(R.id.channel_name)).setText(channelNameText); 199 200 // Set group information if this channel has an associated group. 201 CharSequence groupName = null; 202 if (mSingleNotificationChannel != null && mSingleNotificationChannel.getGroup() != null) { 203 final NotificationChannelGroup notificationChannelGroup = 204 iNotificationManager.getNotificationChannelGroupForPackage( 205 mSingleNotificationChannel.getGroup(), pkg, mAppUid); 206 if (notificationChannelGroup != null) { 207 groupName = notificationChannelGroup.getName(); 208 } 209 } 210 TextView groupNameView = ((TextView) findViewById(R.id.group_name)); 211 TextView groupDividerView = ((TextView) findViewById(R.id.pkg_group_divider)); 212 if (groupName != null) { 213 groupNameView.setText(groupName); 214 groupNameView.setVisibility(View.VISIBLE); 215 groupDividerView.setVisibility(View.VISIBLE); 216 } else { 217 groupNameView.setVisibility(View.GONE); 218 groupDividerView.setVisibility(View.GONE); 219 } 220 221 boolean nonBlockable = false; 222 try { 223 final PackageInfo pkgInfo = pm.getPackageInfo(pkg, PackageManager.GET_SIGNATURES); 224 nonBlockable = Utils.isSystemPackage(getResources(), pm, pkgInfo) 225 && (mSingleNotificationChannel == null 226 || !mSingleNotificationChannel.isBlockableSystem()); 227 } catch (PackageManager.NameNotFoundException e) { 228 // unlikely. 229 } 230 if (nonBlockablePkgs != null) { 231 nonBlockable |= nonBlockablePkgs.contains(pkg); 232 } 233 234 bindButtons(nonBlockable); 235 236 // Top-level importance group 237 mChannelDisabledView = findViewById(R.id.channel_disabled); 238 updateSecondaryText(); 239 240 // Settings button. 241 final TextView settingsButton = (TextView) findViewById(R.id.more_settings); 242 if (mAppUid >= 0 && onSettingsClick != null) { 243 settingsButton.setVisibility(View.VISIBLE); 244 final int appUidF = mAppUid; 245 settingsButton.setOnClickListener( 246 (View view) -> { 247 onSettingsClick.onClick(view, mSingleNotificationChannel, appUidF); 248 }); 249 if (numTotalChannels > 1) { 250 settingsButton.setText(R.string.notification_all_categories); 251 } else { 252 settingsButton.setText(R.string.notification_more_settings); 253 } 254 255 } else { 256 settingsButton.setVisibility(View.GONE); 257 } 258 259 // Done button. 260 final TextView doneButton = (TextView) findViewById(R.id.done); 261 doneButton.setText(R.string.notification_done); 262 doneButton.setOnClickListener(onDoneClick); 263 264 // Optional settings link 265 updateAppSettingsLink(); 266 } 267 hasImportanceChanged()268 private boolean hasImportanceChanged() { 269 return mSingleNotificationChannel != null && 270 mChannelEnabledSwitch != null && 271 mStartingUserImportance != getSelectedImportance(); 272 } 273 saveImportance()274 private void saveImportance() { 275 if (!hasImportanceChanged()) { 276 return; 277 } 278 final int selectedImportance = getSelectedImportance(); 279 MetricsLogger.action(mContext, MetricsEvent.ACTION_SAVE_IMPORTANCE, 280 selectedImportance - mStartingUserImportance); 281 mSingleNotificationChannel.setImportance(selectedImportance); 282 mSingleNotificationChannel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE); 283 try { 284 mINotificationManager.updateNotificationChannelForPackage( 285 mPkg, mAppUid, mSingleNotificationChannel); 286 } catch (RemoteException e) { 287 // :( 288 } 289 } 290 getSelectedImportance()291 private int getSelectedImportance() { 292 if (!mChannelEnabledSwitch.isChecked()) { 293 return IMPORTANCE_NONE; 294 } else { 295 return mStartingUserImportance; 296 } 297 } 298 bindButtons(final boolean nonBlockable)299 private void bindButtons(final boolean nonBlockable) { 300 // Enabled Switch 301 mChannelEnabledSwitch = (Switch) findViewById(R.id.channel_enabled_switch); 302 mChannelEnabledSwitch.setChecked( 303 mStartingUserImportance != IMPORTANCE_NONE); 304 final boolean visible = !nonBlockable && mSingleNotificationChannel != null; 305 mChannelEnabledSwitch.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); 306 307 // Callback when checked. 308 mChannelEnabledSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { 309 if (mGutsContainer != null) { 310 mGutsContainer.resetFalsingCheck(); 311 } 312 updateSecondaryText(); 313 updateAppSettingsLink(); 314 }); 315 } 316 317 @Override onInitializeAccessibilityEvent(AccessibilityEvent event)318 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 319 super.onInitializeAccessibilityEvent(event); 320 if (mGutsContainer != null && 321 event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { 322 if (mGutsContainer.isExposed()) { 323 event.getText().add(mContext.getString( 324 R.string.notification_channel_controls_opened_accessibility, mAppName)); 325 } else { 326 event.getText().add(mContext.getString( 327 R.string.notification_channel_controls_closed_accessibility, mAppName)); 328 } 329 } 330 } 331 updateSecondaryText()332 private void updateSecondaryText() { 333 final boolean disabled = mSingleNotificationChannel != null && 334 getSelectedImportance() == IMPORTANCE_NONE; 335 if (disabled) { 336 mChannelDisabledView.setVisibility(View.VISIBLE); 337 mNumChannelsView.setVisibility(View.GONE); 338 } else { 339 mChannelDisabledView.setVisibility(View.GONE); 340 mNumChannelsView.setVisibility(mIsSingleDefaultChannel ? View.INVISIBLE : View.VISIBLE); 341 } 342 } 343 updateAppSettingsLink()344 private void updateAppSettingsLink() { 345 mSettingsLinkView = findViewById(R.id.app_settings); 346 Intent settingsIntent = getAppSettingsIntent(mPm, mPkg, mSingleNotificationChannel, 347 mSbn.getId(), mSbn.getTag()); 348 if (settingsIntent != null && getSelectedImportance() != IMPORTANCE_NONE 349 && !TextUtils.isEmpty(mSbn.getNotification().getSettingsText())) { 350 mSettingsLinkView.setVisibility(View.VISIBLE); 351 mSettingsLinkView.setText(mContext.getString(R.string.notification_app_settings, 352 mSbn.getNotification().getSettingsText())); 353 mSettingsLinkView.setOnClickListener((View view) -> { 354 mAppSettingsClickListener.onClick(view, settingsIntent); 355 }); 356 } else { 357 mSettingsLinkView.setVisibility(View.GONE); 358 } 359 } 360 getAppSettingsIntent(PackageManager pm, String packageName, NotificationChannel channel, int id, String tag)361 private Intent getAppSettingsIntent(PackageManager pm, String packageName, 362 NotificationChannel channel, int id, String tag) { 363 Intent intent = new Intent(Intent.ACTION_MAIN) 364 .addCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES) 365 .setPackage(packageName); 366 final List<ResolveInfo> resolveInfos = pm.queryIntentActivities( 367 intent, 368 PackageManager.MATCH_DEFAULT_ONLY 369 ); 370 if (resolveInfos == null || resolveInfos.size() == 0 || resolveInfos.get(0) == null) { 371 return null; 372 } 373 final ActivityInfo activityInfo = resolveInfos.get(0).activityInfo; 374 intent.setClassName(activityInfo.packageName, activityInfo.name); 375 if (channel != null) { 376 intent.putExtra(Notification.EXTRA_CHANNEL_ID, channel.getId()); 377 } 378 intent.putExtra(Notification.EXTRA_NOTIFICATION_ID, id); 379 intent.putExtra(Notification.EXTRA_NOTIFICATION_TAG, tag); 380 return intent; 381 } 382 383 @Override setGutsParent(NotificationGuts guts)384 public void setGutsParent(NotificationGuts guts) { 385 mGutsContainer = guts; 386 } 387 388 @Override willBeRemoved()389 public boolean willBeRemoved() { 390 return mChannelEnabledSwitch != null && !mChannelEnabledSwitch.isChecked(); 391 } 392 393 @Override getContentView()394 public View getContentView() { 395 return this; 396 } 397 398 @Override handleCloseControls(boolean save, boolean force)399 public boolean handleCloseControls(boolean save, boolean force) { 400 if (save && hasImportanceChanged()) { 401 if (mCheckSaveListener != null) { 402 mCheckSaveListener.checkSave(() -> { saveImportance(); }); 403 } else { 404 saveImportance(); 405 } 406 } 407 return false; 408 } 409 410 @Override getActualHeight()411 public int getActualHeight() { 412 return getHeight(); 413 } 414 } 415