diff options
25 files changed, 1183 insertions, 4 deletions
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index b95a402013ec..76226888bf1a 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -5151,10 +5151,18 @@ public class Notification implements Parcelable bindHeaderChronometerAndTime(contentView, p); bindProfileBadge(contentView, p); bindAlertedIcon(contentView, p); + bindActivePermissions(contentView, p); bindExpandButton(contentView, p); mN.mUsesStandardHeader = true; } + private void bindActivePermissions(RemoteViews contentView, StandardTemplateParams p) { + int color = getNeutralColor(p); + contentView.setDrawableTint(R.id.camera, false, color, PorterDuff.Mode.SRC_ATOP); + contentView.setDrawableTint(R.id.mic, false, color, PorterDuff.Mode.SRC_ATOP); + contentView.setDrawableTint(R.id.overlay, false, color, PorterDuff.Mode.SRC_ATOP); + } + private void bindExpandButton(RemoteViews contentView, StandardTemplateParams p) { int color = isColorized(p) ? getPrimaryTextColor(p) : getSecondaryTextColor(p); contentView.setDrawableTint(R.id.expand_button, false, color, diff --git a/core/java/android/view/NotificationHeaderView.java b/core/java/android/view/NotificationHeaderView.java index 7a467d6ad73f..0c50cb782c24 100644 --- a/core/java/android/view/NotificationHeaderView.java +++ b/core/java/android/view/NotificationHeaderView.java @@ -52,11 +52,13 @@ public class NotificationHeaderView extends ViewGroup { private View mHeaderText; private View mSecondaryHeaderText; private OnClickListener mExpandClickListener; + private OnClickListener mAppOpsListener; private HeaderTouchListener mTouchListener = new HeaderTouchListener(); private LinearLayout mTransferChip; private NotificationExpandButton mExpandButton; private CachingIconView mIcon; private View mProfileBadge; + private View mAppOps; private boolean mExpanded; private boolean mShowExpandButtonAtEnd; private boolean mShowWorkBadgeAtEnd; @@ -113,6 +115,7 @@ public class NotificationHeaderView extends ViewGroup { mExpandButton = findViewById(com.android.internal.R.id.expand_button); mIcon = findViewById(com.android.internal.R.id.icon); mProfileBadge = findViewById(com.android.internal.R.id.profile_badge); + mAppOps = findViewById(com.android.internal.R.id.app_ops); } @Override @@ -140,6 +143,7 @@ public class NotificationHeaderView extends ViewGroup { // Icons that should go at the end if ((child == mExpandButton && mShowExpandButtonAtEnd) || child == mProfileBadge + || child == mAppOps || child == mTransferChip) { iconWidth += lp.leftMargin + lp.rightMargin + child.getMeasuredWidth(); } else { @@ -204,6 +208,7 @@ public class NotificationHeaderView extends ViewGroup { // Icons that should go at the end if ((child == mExpandButton && mShowExpandButtonAtEnd) || child == mProfileBadge + || child == mAppOps || child == mTransferChip) { if (end == getMeasuredWidth()) { layoutRight = end - mContentEndMargin; @@ -272,10 +277,22 @@ public class NotificationHeaderView extends ViewGroup { } private void updateTouchListener() { + if (mExpandClickListener == null && mAppOpsListener == null) { + setOnTouchListener(null); + return; + } setOnTouchListener(mTouchListener); mTouchListener.bindTouchRects(); } + /** + * Sets onclick listener for app ops icons. + */ + public void setAppOpsOnClickListener(OnClickListener l) { + mAppOpsListener = l; + updateTouchListener(); + } + @Override public void setOnClickListener(@Nullable OnClickListener l) { mExpandClickListener = l; @@ -363,6 +380,7 @@ public class NotificationHeaderView extends ViewGroup { private final ArrayList<Rect> mTouchRects = new ArrayList<>(); private Rect mExpandButtonRect; + private Rect mAppOpsRect; private int mTouchSlop; private boolean mTrackGesture; private float mDownX; @@ -375,6 +393,8 @@ public class NotificationHeaderView extends ViewGroup { mTouchRects.clear(); addRectAroundView(mIcon); mExpandButtonRect = addRectAroundView(mExpandButton); + mAppOpsRect = addRectAroundView(mAppOps); + setTouchDelegate(new TouchDelegate(mAppOpsRect, mAppOps)); addWidthRect(); mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); } @@ -435,6 +455,11 @@ public class NotificationHeaderView extends ViewGroup { break; case MotionEvent.ACTION_UP: if (mTrackGesture) { + if (mAppOps.isVisibleToUser() && (mAppOpsRect.contains((int) x, (int) y) + || mAppOpsRect.contains((int) mDownX, (int) mDownY))) { + mAppOps.performClick(); + return true; + } mExpandButton.performClick(); } break; diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java index e3a456cc94a4..0791ed3c42ec 100644 --- a/core/java/com/android/internal/widget/ConversationLayout.java +++ b/core/java/com/android/internal/widget/ConversationLayout.java @@ -167,6 +167,8 @@ public class ConversationLayout extends FrameLayout private int mFacePileProtectionWidthExpanded; private boolean mImportantConversation; private TextView mUnreadBadge; + private ViewGroup mAppOps; + private Rect mAppOpsTouchRect = new Rect(); private float mMinTouchSize; private Icon mConversationIcon; private Icon mShortcutIcon; @@ -208,6 +210,7 @@ public class ConversationLayout extends FrameLayout mConversationIconView = findViewById(R.id.conversation_icon); mConversationIconContainer = findViewById(R.id.conversation_icon_container); mIcon = findViewById(R.id.icon); + mAppOps = findViewById(com.android.internal.R.id.app_ops); mMinTouchSize = 48 * getResources().getDisplayMetrics().density; mImportanceRingView = findViewById(R.id.conversation_icon_badge_ring); mConversationIconBadge = findViewById(R.id.conversation_icon_badge); @@ -1163,6 +1166,47 @@ public class ConversationLayout extends FrameLayout } }); } + if (mAppOps.getWidth() > 0) { + + // Let's increase the touch size of the app ops view if it's here + mAppOpsTouchRect.set( + mAppOps.getLeft(), + mAppOps.getTop(), + mAppOps.getRight(), + mAppOps.getBottom()); + for (int i = 0; i < mAppOps.getChildCount(); i++) { + View child = mAppOps.getChildAt(i); + if (child.getVisibility() == GONE) { + continue; + } + // Make sure each child has at least a minTouchSize touch target around it + float childTouchLeft = child.getLeft() + child.getWidth() / 2.0f + - mMinTouchSize / 2.0f; + float childTouchRight = childTouchLeft + mMinTouchSize; + mAppOpsTouchRect.left = (int) Math.min(mAppOpsTouchRect.left, + mAppOps.getLeft() + childTouchLeft); + mAppOpsTouchRect.right = (int) Math.max(mAppOpsTouchRect.right, + mAppOps.getLeft() + childTouchRight); + } + + // Increase the height + int heightIncrease = 0; + if (mAppOpsTouchRect.height() < mMinTouchSize) { + heightIncrease = (int) Math.ceil((mMinTouchSize - mAppOpsTouchRect.height()) + / 2.0f); + } + mAppOpsTouchRect.inset(0, -heightIncrease); + + // Let's adjust the hitrect since app ops isn't a direct child + ViewGroup viewGroup = (ViewGroup) mAppOps.getParent(); + while (viewGroup != this) { + mAppOpsTouchRect.offset(viewGroup.getLeft(), viewGroup.getTop()); + viewGroup = (ViewGroup) viewGroup.getParent(); + } + // + // Extend the size of the app opps to be at least 48dp + setTouchDelegate(new TouchDelegate(mAppOpsTouchRect, mAppOps)); + } } public MessagingLinearLayout getMessagingLinearLayout() { diff --git a/core/res/res/layout/notification_template_header.xml b/core/res/res/layout/notification_template_header.xml index 03e130e8023b..23b8bd34829e 100644 --- a/core/res/res/layout/notification_template_header.xml +++ b/core/res/res/layout/notification_template_header.xml @@ -146,6 +146,43 @@ android:visibility="gone" android:contentDescription="@string/notification_work_profile_content_description" /> + <LinearLayout + android:id="@+id/app_ops" + android:layout_height="match_parent" + android:layout_width="wrap_content" + android:layout_marginStart="6dp" + android:background="?android:selectableItemBackgroundBorderless" + android:orientation="horizontal"> + <ImageView + android:id="@+id/camera" + android:layout_width="?attr/notificationHeaderIconSize" + android:layout_height="?attr/notificationHeaderIconSize" + android:src="@drawable/ic_camera" + android:visibility="gone" + android:focusable="false" + android:contentDescription="@string/notification_appops_camera_active" + /> + <ImageView + android:id="@+id/mic" + android:layout_width="?attr/notificationHeaderIconSize" + android:layout_height="?attr/notificationHeaderIconSize" + android:src="@drawable/ic_mic" + android:layout_marginStart="4dp" + android:visibility="gone" + android:focusable="false" + android:contentDescription="@string/notification_appops_microphone_active" + /> + <ImageView + android:id="@+id/overlay" + android:layout_width="?attr/notificationHeaderIconSize" + android:layout_height="?attr/notificationHeaderIconSize" + android:src="@drawable/ic_alert_window_layer" + android:layout_marginStart="4dp" + android:visibility="gone" + android:focusable="false" + android:contentDescription="@string/notification_appops_overlay_active" + /> + </LinearLayout> <include layout="@layout/notification_material_media_transfer_action" android:id="@+id/media_seamless" diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowPlugin.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowPlugin.java index 7463d3fd3650..8f24e7927e3a 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowPlugin.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowPlugin.java @@ -75,6 +75,11 @@ public interface NotificationMenuRowPlugin extends Plugin { public MenuItem getLongpressMenuItem(Context context); /** + * @return the {@link MenuItem} to display when app ops icons are pressed. + */ + public MenuItem getAppOpsMenuItem(Context context); + + /** * @return the {@link MenuItem} to display when snooze item is pressed. */ public MenuItem getSnoozeMenuItem(Context context); diff --git a/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java b/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java index 5f8815665d88..2deeb1230f09 100644 --- a/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java +++ b/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java @@ -39,15 +39,21 @@ import javax.inject.Singleton; */ @Singleton public class ForegroundServiceController { - public static final int[] APP_OPS = new int[] {AppOpsManager.OP_SYSTEM_ALERT_WINDOW}; + public static final int[] APP_OPS = new int[] {AppOpsManager.OP_CAMERA, + AppOpsManager.OP_SYSTEM_ALERT_WINDOW, + AppOpsManager.OP_RECORD_AUDIO, + AppOpsManager.OP_COARSE_LOCATION, + AppOpsManager.OP_FINE_LOCATION}; private final SparseArray<ForegroundServicesUserState> mUserServices = new SparseArray<>(); private final Object mMutex = new Object(); + private final NotificationEntryManager mEntryManager; private final Handler mMainHandler; @Inject - public ForegroundServiceController(AppOpsController appOpsController, - @Main Handler mainHandler) { + public ForegroundServiceController(NotificationEntryManager entryManager, + AppOpsController appOpsController, @Main Handler mainHandler) { + mEntryManager = entryManager; mMainHandler = mainHandler; appOpsController.addCallback(APP_OPS, (code, uid, packageName, active) -> { mMainHandler.post(() -> { @@ -81,6 +87,19 @@ public class ForegroundServiceController { } /** + * Returns the keys for notifications from this package using the standard template, + * if they exist. + */ + @Nullable + public ArraySet<String> getStandardLayoutKeys(int userId, String pkg) { + synchronized (mMutex) { + final ForegroundServicesUserState services = mUserServices.get(userId); + if (services == null) return null; + return services.getStandardLayoutKeys(pkg); + } + } + + /** * Gets active app ops for this user and package */ @Nullable @@ -121,6 +140,31 @@ public class ForegroundServiceController { userServices.removeOp(packageName, appOpCode); } } + + // TODO: (b/145659174) remove when moving to NewNotifPipeline. Replaced by + // AppOpsCoordinator + // Update appOps if there are associated pending or visible notifications + final Set<String> notificationKeys = getStandardLayoutKeys(userId, packageName); + if (notificationKeys != null) { + boolean changed = false; + for (String key : notificationKeys) { + final NotificationEntry entry = mEntryManager.getPendingOrActiveNotif(key); + if (entry != null + && uid == entry.getSbn().getUid() + && packageName.equals(entry.getSbn().getPackageName())) { + synchronized (entry.mActiveAppOps) { + if (active) { + changed |= entry.mActiveAppOps.add(appOpCode); + } else { + changed |= entry.mActiveAppOps.remove(appOpCode); + } + } + } + } + if (changed) { + mEntryManager.updateNotifications("appOpChanged pkg=" + packageName); + } + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java b/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java index 1515272569d6..bb445832da93 100644 --- a/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java +++ b/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java @@ -172,8 +172,24 @@ public class ForegroundServiceNotificationListener { sbn.getPackageName(), sbn.getKey()); } } + tagAppOps(entry); return true; }, true /* create if not found */); } + + // TODO: (b/145659174) remove when moving to NewNotifPipeline. Replaced by + // AppOpsCoordinator + private void tagAppOps(NotificationEntry entry) { + final StatusBarNotification sbn = entry.getSbn(); + ArraySet<Integer> activeOps = mForegroundServiceController.getAppOps( + sbn.getUserId(), + sbn.getPackageName()); + synchronized (entry.mActiveAppOps) { + entry.mActiveAppOps.clear(); + if (activeOps != null) { + entry.mActiveAppOps.addAll(activeOps); + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java index 6b8afffa065a..5bee9a762f6f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java @@ -486,6 +486,7 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle } } + row.showAppOpsIcons(entry.mActiveAppOps); row.setLastAudiblyAlertedMs(entry.getLastAudiblyAlertedMs()); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinator.java index 84108b196d72..4b244bb18975 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinator.java @@ -76,9 +76,14 @@ public class AppOpsCoordinator implements Coordinator { // extend the lifetime of foreground notification services to show for at least 5 seconds mNotifPipeline.addNotificationLifetimeExtender(mForegroundLifetimeExtender); + // listen for new notifications to add appOps + mNotifPipeline.addCollectionListener(mNotifCollectionListener); + // filter out foreground service notifications that aren't necessary anymore mNotifPipeline.addPreGroupFilter(mNotifFilter); + // when appOps change, update any relevant notifications to update appOps for + mAppOpsController.addCallback(ForegroundServiceController.APP_OPS, this::onAppOpsChanged); } /** @@ -169,4 +174,82 @@ public class AppOpsCoordinator implements Coordinator { } } }; + + /** + * Adds appOps to incoming and updating notifications + */ + private NotifCollectionListener mNotifCollectionListener = new NotifCollectionListener() { + @Override + public void onEntryAdded(NotificationEntry entry) { + tagAppOps(entry); + } + + @Override + public void onEntryUpdated(NotificationEntry entry) { + tagAppOps(entry); + } + + private void tagAppOps(NotificationEntry entry) { + final StatusBarNotification sbn = entry.getSbn(); + // note: requires that the ForegroundServiceController is updating their appOps first + ArraySet<Integer> activeOps = + mForegroundServiceController.getAppOps( + sbn.getUser().getIdentifier(), + sbn.getPackageName()); + + entry.mActiveAppOps.clear(); + if (activeOps != null) { + entry.mActiveAppOps.addAll(activeOps); + } + } + }; + + private void onAppOpsChanged(int code, int uid, String packageName, boolean active) { + mMainExecutor.execute(() -> handleAppOpsChanged(code, uid, packageName, active)); + } + + /** + * Update the appOp for the posted notification associated with the current foreground service + * + * @param code code for appOp to add/remove + * @param uid of user the notification is sent to + * @param packageName package that created the notification + * @param active whether the appOpCode is active or not + */ + private void handleAppOpsChanged(int code, int uid, String packageName, boolean active) { + Assert.isMainThread(); + + int userId = UserHandle.getUserId(uid); + + // Update appOps of the app's posted notifications with standard layouts + final ArraySet<String> notifKeys = + mForegroundServiceController.getStandardLayoutKeys(userId, packageName); + if (notifKeys != null) { + boolean changed = false; + for (int i = 0; i < notifKeys.size(); i++) { + final NotificationEntry entry = findNotificationEntryWithKey(notifKeys.valueAt(i)); + if (entry != null + && uid == entry.getSbn().getUid() + && packageName.equals(entry.getSbn().getPackageName())) { + if (active) { + changed |= entry.mActiveAppOps.add(code); + } else { + changed |= entry.mActiveAppOps.remove(code); + } + } + } + if (changed) { + mNotifFilter.invalidateList(); + } + } + } + + private NotificationEntry findNotificationEntryWithKey(String key) { + for (NotificationEntry entry : mNotifPipeline.getAllNotifs()) { + if (entry.getKey().equals(key)) { + return entry; + } + } + return null; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AppOpsInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AppOpsInfo.java new file mode 100644 index 000000000000..28c53dc6d9b2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AppOpsInfo.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.statusbar.notification.row; + +import android.app.AppOpsManager; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.service.notification.StatusBarNotification; +import android.util.ArraySet; +import android.util.AttributeSet; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.UiEventLogger; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.R; + +/** + * The guts of a notification revealed when performing a long press. + */ +public class AppOpsInfo extends LinearLayout implements NotificationGuts.GutsContent { + private static final String TAG = "AppOpsGuts"; + + private PackageManager mPm; + + private String mPkg; + private String mAppName; + private int mAppUid; + private StatusBarNotification mSbn; + private ArraySet<Integer> mAppOps; + private MetricsLogger mMetricsLogger; + private OnSettingsClickListener mOnSettingsClickListener; + private NotificationGuts mGutsContainer; + private UiEventLogger mUiEventLogger; + + private OnClickListener mOnOk = v -> { + mGutsContainer.closeControls(v, false); + }; + + public AppOpsInfo(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public interface OnSettingsClickListener { + void onClick(View v, String pkg, int uid, ArraySet<Integer> ops); + } + + public void bindGuts(final PackageManager pm, + final OnSettingsClickListener onSettingsClick, + final StatusBarNotification sbn, + final UiEventLogger uiEventLogger, + ArraySet<Integer> activeOps) { + mPkg = sbn.getPackageName(); + mSbn = sbn; + mPm = pm; + mAppName = mPkg; + mOnSettingsClickListener = onSettingsClick; + mAppOps = activeOps; + mUiEventLogger = uiEventLogger; + + bindHeader(); + bindPrompt(); + bindButtons(); + + logUiEvent(NotificationAppOpsEvent.NOTIFICATION_APP_OPS_OPEN); + mMetricsLogger = new MetricsLogger(); + mMetricsLogger.visibility(MetricsEvent.APP_OPS_GUTS, true); + } + + private void bindHeader() { + // Package name + Drawable pkgicon = null; + ApplicationInfo info; + try { + info = mPm.getApplicationInfo(mPkg, + PackageManager.MATCH_UNINSTALLED_PACKAGES + | PackageManager.MATCH_DISABLED_COMPONENTS + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE + | PackageManager.MATCH_DIRECT_BOOT_AWARE); + if (info != null) { + mAppUid = mSbn.getUid(); + mAppName = String.valueOf(mPm.getApplicationLabel(info)); + pkgicon = mPm.getApplicationIcon(info); + } + } catch (PackageManager.NameNotFoundException e) { + // app is gone, just show package name and generic icon + pkgicon = mPm.getDefaultActivityIcon(); + } + ((ImageView) findViewById(R.id.pkgicon)).setImageDrawable(pkgicon); + ((TextView) findViewById(R.id.pkgname)).setText(mAppName); + } + + private void bindPrompt() { + final TextView prompt = findViewById(R.id.prompt); + prompt.setText(getPrompt()); + } + + private void bindButtons() { + View settings = findViewById(R.id.settings); + settings.setOnClickListener((View view) -> { + mOnSettingsClickListener.onClick(view, mPkg, mAppUid, mAppOps); + }); + TextView ok = findViewById(R.id.ok); + ok.setOnClickListener(mOnOk); + ok.setAccessibilityDelegate(mGutsContainer.getAccessibilityDelegate()); + } + + private String getPrompt() { + if (mAppOps == null || mAppOps.size() == 0) { + return ""; + } else if (mAppOps.size() == 1) { + if (mAppOps.contains(AppOpsManager.OP_CAMERA)) { + return mContext.getString(R.string.appops_camera); + } else if (mAppOps.contains(AppOpsManager.OP_RECORD_AUDIO)) { + return mContext.getString(R.string.appops_microphone); + } else { + return mContext.getString(R.string.appops_overlay); + } + } else if (mAppOps.size() == 2) { + if (mAppOps.contains(AppOpsManager.OP_CAMERA)) { + if (mAppOps.contains(AppOpsManager.OP_RECORD_AUDIO)) { + return mContext.getString(R.string.appops_camera_mic); + } else { + return mContext.getString(R.string.appops_camera_overlay); + } + } else { + return mContext.getString(R.string.appops_mic_overlay); + } + } else { + return mContext.getString(R.string.appops_camera_mic_overlay); + } + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + if (mGutsContainer != null && + event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { + if (mGutsContainer.isExposed()) { + event.getText().add(mContext.getString( + R.string.notification_channel_controls_opened_accessibility, mAppName)); + } else { + event.getText().add(mContext.getString( + R.string.notification_channel_controls_closed_accessibility, mAppName)); + } + } + } + + @Override + public void setGutsParent(NotificationGuts guts) { + mGutsContainer = guts; + } + + @Override + public boolean willBeRemoved() { + return false; + } + + @Override + public boolean shouldBeSaved() { + return false; + } + + @Override + public boolean needsFalsingProtection() { + return false; + } + + @Override + public View getContentView() { + return this; + } + + @Override + public boolean handleCloseControls(boolean save, boolean force) { + logUiEvent(NotificationAppOpsEvent.NOTIFICATION_APP_OPS_CLOSE); + if (mMetricsLogger != null) { + mMetricsLogger.visibility(MetricsEvent.APP_OPS_GUTS, false); + } + return false; + } + + @Override + public int getActualHeight() { + return getHeight(); + } + + private void logUiEvent(NotificationAppOpsEvent event) { + if (mSbn != null) { + mUiEventLogger.logWithInstanceId(event, + mSbn.getUid(), mSbn.getPackageName(), mSbn.getInstanceId()); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 8ead7bf2322f..94e12e82f850 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -239,6 +239,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private boolean mShowNoBackground; private ExpandableNotificationRow mNotificationParent; private OnExpandClickListener mOnExpandClickListener; + private View.OnClickListener mOnAppOpsClickListener; // Listener will be called when receiving a long click event. // Use #setLongPressPosition to optionally assign positional data with the long press. @@ -1142,6 +1143,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView items.add(NotificationMenuRow.createPartialConversationItem(mContext)); items.add(NotificationMenuRow.createInfoItem(mContext)); items.add(NotificationMenuRow.createSnoozeItem(mContext)); + items.add(NotificationMenuRow.createAppOpsItem(mContext)); mMenuRow.setMenuItems(items); } if (existed) { @@ -1607,6 +1609,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView RowContentBindStage rowContentBindStage, OnExpandClickListener onExpandClickListener, NotificationMediaManager notificationMediaManager, + OnAppOpsClickListener onAppOpsClickListener, FalsingManager falsingManager, StatusBarStateController statusBarStateController, PeopleNotificationIdentifier peopleNotificationIdentifier) { @@ -1626,6 +1629,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mRowContentBindStage = rowContentBindStage; mOnExpandClickListener = onExpandClickListener; mMediaManager = notificationMediaManager; + setAppOpsOnClickListener(onAppOpsClickListener); mFalsingManager = falsingManager; mStatusbarStateController = statusBarStateController; mPeopleNotificationIdentifier = peopleNotificationIdentifier; @@ -1682,6 +1686,14 @@ public class ExpandableNotificationRow extends ActivatableNotificationView requestLayout(); } + public void showAppOpsIcons(ArraySet<Integer> activeOps) { + if (mIsSummaryWithChildren) { + mChildrenContainer.showAppOpsIcons(activeOps); + } + mPrivateLayout.showAppOpsIcons(activeOps); + mPublicLayout.showAppOpsIcons(activeOps); + } + /** Sets the last time the notification being displayed audibly alerted the user. */ public void setLastAudiblyAlertedMs(long lastAudiblyAlertedMs) { if (NotificationUtils.useNewInterruptionModel(mContext)) { @@ -1710,6 +1722,24 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mPublicLayout.setRecentlyAudiblyAlerted(audiblyAlertedRecently); } + public View.OnClickListener getAppOpsOnClickListener() { + return mOnAppOpsClickListener; + } + + void setAppOpsOnClickListener(ExpandableNotificationRow.OnAppOpsClickListener l) { + mOnAppOpsClickListener = v -> { + createMenu(); + NotificationMenuRowPlugin provider = getProvider(); + if (provider == null) { + return; + } + MenuItem menuItem = provider.getAppOpsMenuItem(mContext); + if (menuItem != null) { + l.onClick(this, v.getWidth() / 2, v.getHeight() / 2, menuItem); + } + }; + } + @Override protected void onFinishInflate() { super.onFinishInflate(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java index b132caf56265..7a6109d2ce78 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java @@ -63,6 +63,7 @@ public class ExpandableNotificationRowController { private final ExpandableNotificationRow.ExpansionLogger mExpansionLogger = this::logNotificationExpansion; + private final ExpandableNotificationRow.OnAppOpsClickListener mOnAppOpsClickListener; private final NotificationGutsManager mNotificationGutsManager; private Runnable mOnDismissRunnable; private final FalsingManager mFalsingManager; @@ -100,6 +101,7 @@ public class ExpandableNotificationRowController { mStatusBarStateController = statusBarStateController; mNotificationGutsManager = notificationGutsManager; mOnDismissRunnable = onDismissRunnable; + mOnAppOpsClickListener = mNotificationGutsManager::openGuts; mAllowLongPress = allowLongPress; mFalsingManager = falsingManager; mPeopleNotificationIdentifier = peopleNotificationIdentifier; @@ -120,6 +122,7 @@ public class ExpandableNotificationRowController { mRowContentBindStage, mOnExpandClickListener, mMediaManager, + mOnAppOpsClickListener, mFalsingManager, mStatusBarStateController, mPeopleNotificationIdentifier diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index 41a9b187129c..1f5b063b0aa2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -1567,6 +1567,18 @@ public class NotificationContentView extends FrameLayout { return header; } + public void showAppOpsIcons(ArraySet<Integer> activeOps) { + if (mContractedChild != null) { + mContractedWrapper.showAppOpsIcons(activeOps); + } + if (mExpandedChild != null) { + mExpandedWrapper.showAppOpsIcons(activeOps); + } + if (mHeadsUpChild != null) { + mHeadsUpWrapper.showAppOpsIcons(activeOps); + } + } + /** Sets whether the notification being displayed audibly alerted the user. */ public void setRecentlyAudiblyAlerted(boolean audiblyAlerted) { if (mContractedChild != null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java index 3eed18a90836..24883f51a984 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java @@ -264,6 +264,8 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx try { if (gutsView instanceof NotificationSnooze) { initializeSnoozeView(row, (NotificationSnooze) gutsView); + } else if (gutsView instanceof AppOpsInfo) { + initializeAppOpsInfo(row, (AppOpsInfo) gutsView); } else if (gutsView instanceof NotificationInfo) { initializeNotificationInfo(row, (NotificationInfo) gutsView); } else if (gutsView instanceof NotificationConversationInfo) { @@ -301,6 +303,36 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx } /** + * Sets up the {@link AppOpsInfo} inside the notification row's guts. + * + * @param row view to set up the guts for + * @param appOpsInfoView view to set up/bind within {@code row} + */ + private void initializeAppOpsInfo( + final ExpandableNotificationRow row, + AppOpsInfo appOpsInfoView) { + NotificationGuts guts = row.getGuts(); + StatusBarNotification sbn = row.getEntry().getSbn(); + UserHandle userHandle = sbn.getUser(); + PackageManager pmUser = StatusBar.getPackageManagerForUser(mContext, + userHandle.getIdentifier()); + + AppOpsInfo.OnSettingsClickListener onSettingsClick = + (View v, String pkg, int uid, ArraySet<Integer> ops) -> { + mUiEventLogger.logWithInstanceId( + NotificationAppOpsEvent.NOTIFICATION_APP_OPS_SETTINGS_CLICK, + sbn.getUid(), sbn.getPackageName(), sbn.getInstanceId()); + mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_OPS_GUTS_SETTINGS); + guts.resetFalsingCheck(); + startAppOpsSettingsActivity(pkg, uid, ops, row); + }; + if (!row.getEntry().mActiveAppOps.isEmpty()) { + appOpsInfoView.bindGuts(pmUser, onSettingsClick, sbn, mUiEventLogger, + row.getEntry().mActiveAppOps); + } + } + + /** * Sets up the {@link NotificationInfo} inside the notification row's guts. * @param row view to set up the guts for * @param notificationInfoView view to set up/bind within {@code row} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java index a167925a6358..5e1e3b255867 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java @@ -76,6 +76,7 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl private Context mContext; private FrameLayout mMenuContainer; private NotificationMenuItem mInfoItem; + private MenuItem mAppOpsItem; private MenuItem mSnoozeItem; private ArrayList<MenuItem> mLeftMenuItems; private ArrayList<MenuItem> mRightMenuItems; @@ -137,6 +138,11 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl } @Override + public MenuItem getAppOpsMenuItem(Context context) { + return mAppOpsItem; + } + + @Override public MenuItem getSnoozeMenuItem(Context context) { return mSnoozeItem; } @@ -258,6 +264,7 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl // Only show snooze for non-foreground notifications, and if the setting is on mSnoozeItem = createSnoozeItem(mContext); } + mAppOpsItem = createAppOpsItem(mContext); NotificationEntry entry = mParent.getEntry(); int personNotifType = mPeopleNotificationIdentifier .getPeopleNotificationType(entry.getSbn(), entry.getRanking()); @@ -273,6 +280,7 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl mRightMenuItems.add(mSnoozeItem); } mRightMenuItems.add(mInfoItem); + mRightMenuItems.add(mAppOpsItem); mLeftMenuItems.addAll(mRightMenuItems); populateMenuViews(); @@ -680,6 +688,14 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl R.drawable.ic_settings); } + static MenuItem createAppOpsItem(Context context) { + AppOpsInfo appOpsContent = (AppOpsInfo) LayoutInflater.from(context).inflate( + R.layout.app_ops_info, null, false); + MenuItem info = new NotificationMenuItem(context, null, appOpsContent, + -1 /*don't show in slow swipe menu */); + return info; + } + private void addMenuView(MenuItem item, ViewGroup parent) { View menuView = item.getMenuView(); if (menuView != null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java index 86fc352f6fe9..c747a7c300b7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.row.wrapper; import static com.android.systemui.statusbar.notification.TransformState.TRANSFORM_Y; +import android.app.AppOpsManager; import android.app.Notification; import android.content.Context; import android.util.ArraySet; @@ -63,6 +64,10 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { private TextView mHeaderText; private TextView mAppNameText; private ImageView mWorkProfileImage; + private View mCameraIcon; + private View mMicIcon; + private View mOverlayIcon; + private View mAppOps; private View mAudiblyAlertedIcon; private FrameLayout mIconContainer; @@ -103,6 +108,7 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { } }, TRANSFORMING_VIEW_TITLE); resolveHeaderViews(); + addAppOpsOnClickListener(row); } protected void resolveHeaderViews() { @@ -113,6 +119,10 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { mExpandButton = mView.findViewById(com.android.internal.R.id.expand_button); mWorkProfileImage = mView.findViewById(com.android.internal.R.id.profile_badge); mNotificationHeader = mView.findViewById(com.android.internal.R.id.notification_header); + mCameraIcon = mView.findViewById(com.android.internal.R.id.camera); + mMicIcon = mView.findViewById(com.android.internal.R.id.mic); + mOverlayIcon = mView.findViewById(com.android.internal.R.id.overlay); + mAppOps = mView.findViewById(com.android.internal.R.id.app_ops); mAudiblyAlertedIcon = mView.findViewById(com.android.internal.R.id.alerted_icon); if (mNotificationHeader != null) { mNotificationHeader.setShowExpandButtonAtEnd(mShowExpandButtonAtEnd); @@ -120,6 +130,38 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { } } + private void addAppOpsOnClickListener(ExpandableNotificationRow row) { + View.OnClickListener listener = row.getAppOpsOnClickListener(); + if (mNotificationHeader != null) { + mNotificationHeader.setAppOpsOnClickListener(listener); + } + if (mAppOps != null) { + mAppOps.setOnClickListener(listener); + } + } + + /** + * Shows or hides 'app op in use' icons based on app usage. + */ + @Override + public void showAppOpsIcons(ArraySet<Integer> appOps) { + if (appOps == null) { + return; + } + if (mOverlayIcon != null) { + mOverlayIcon.setVisibility(appOps.contains(AppOpsManager.OP_SYSTEM_ALERT_WINDOW) + ? View.VISIBLE : View.GONE); + } + if (mCameraIcon != null) { + mCameraIcon.setVisibility(appOps.contains(AppOpsManager.OP_CAMERA) + ? View.VISIBLE : View.GONE); + } + if (mMicIcon != null) { + mMicIcon.setVisibility(appOps.contains(AppOpsManager.OP_RECORD_AUDIO) + ? View.VISIBLE : View.GONE); + } + } + @Override public void onContentUpdated(ExpandableNotificationRow row) { super.onContentUpdated(row); @@ -243,6 +285,15 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_TITLE, mHeaderText); } + if (mCameraIcon != null) { + mTransformationHelper.addViewTransformingToSimilar(mCameraIcon); + } + if (mMicIcon != null) { + mTransformationHelper.addViewTransformingToSimilar(mMicIcon); + } + if (mOverlayIcon != null) { + mTransformationHelper.addViewTransformingToSimilar(mOverlayIcon); + } if (mAudiblyAlertedIcon != null) { mTransformationHelper.addViewTransformingToSimilar(mAudiblyAlertedIcon); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java index 4bf279444462..30080e3d8cc2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java @@ -96,6 +96,14 @@ public abstract class NotificationViewWrapper implements TransformableView { public void onContentUpdated(ExpandableNotificationRow row) { } + /** + * Show a set of app opp icons in the layout. + * + * @param appOps which app ops to show + */ + public void showAppOpsIcons(ArraySet<Integer> appOps) { + } + public void onReinflated() { if (shouldClearBackgroundOnReapply()) { mBackgroundColor = 0; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java index 5edcde1cd3c7..99691b710cc2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java @@ -1302,6 +1302,20 @@ public class NotificationChildrenContainer extends ViewGroup { mCurrentHeaderTranslation = (int) ((1.0f - headerVisibleAmount) * mTranslationForHeader); } + /** + * Show a set of app opp icons in the layout. + * + * @param appOps which app ops to show + */ + public void showAppOpsIcons(ArraySet<Integer> appOps) { + if (mNotificationHeaderWrapper != null) { + mNotificationHeaderWrapper.showAppOpsIcons(appOps); + } + if (mNotificationHeaderWrapperLowPriority != null) { + mNotificationHeaderWrapperLowPriority.showAppOpsIcons(appOps); + } + } + public void setRecentlyAudiblyAlerted(boolean audiblyAlertedRecently) { if (mNotificationHeaderWrapper != null) { mNotificationHeaderWrapper.setRecentlyAudiblyAlerted(audiblyAlertedRecently); diff --git a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java index e967a5d607eb..60f0cd9da5f2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java @@ -82,7 +82,8 @@ public class ForegroundServiceControllerTest extends SysuiTestCase { allowTestableLooperAsMainThread(); MockitoAnnotations.initMocks(this); - mFsc = new ForegroundServiceController(mAppOpsController, mMainHandler); + mFsc = new ForegroundServiceController( + mEntryManager, mAppOpsController, mMainHandler); mListener = new ForegroundServiceNotificationListener( mContext, mFsc, mEntryManager, mNotifPipeline, mock(ForegroundServiceLifetimeExtender.class), mClock); @@ -114,6 +115,85 @@ public class ForegroundServiceControllerTest extends SysuiTestCase { } @Test + public void testAppOps_appOpChangedBeforeNotificationExists() { + // GIVEN app op exists, but notification doesn't exist in NEM yet + NotificationEntry entry = createFgEntry(); + mFsc.onAppOpChanged( + AppOpsManager.OP_CAMERA, + entry.getSbn().getUid(), + entry.getSbn().getPackageName(), + true); + assertFalse(entry.mActiveAppOps.contains(AppOpsManager.OP_CAMERA)); + + // WHEN the notification is added + mEntryListener.onPendingEntryAdded(entry); + + // THEN the app op is added to the entry + Assert.assertTrue(entry.mActiveAppOps.contains(AppOpsManager.OP_CAMERA)); + } + + @Test + public void testAppOps_appOpAddedToForegroundNotif() { + // GIVEN a notification associated with a foreground service + NotificationEntry entry = addFgEntry(); + when(mEntryManager.getPendingOrActiveNotif(entry.getKey())).thenReturn(entry); + + // WHEN we are notified of a new app op for this notification + mFsc.onAppOpChanged( + AppOpsManager.OP_CAMERA, + entry.getSbn().getUid(), + entry.getSbn().getPackageName(), + true); + + // THEN the app op is added to the entry + Assert.assertTrue(entry.mActiveAppOps.contains(AppOpsManager.OP_CAMERA)); + + // THEN notification views are updated since the notification is visible + verify(mEntryManager, times(1)).updateNotifications(anyString()); + } + + @Test + public void testAppOpsAlreadyAdded() { + // GIVEN a foreground service associated notification that already has the correct app op + NotificationEntry entry = addFgEntry(); + entry.mActiveAppOps.add(AppOpsManager.OP_CAMERA); + when(mEntryManager.getPendingOrActiveNotif(entry.getKey())).thenReturn(entry); + + // WHEN we are notified of the same app op for this notification + mFsc.onAppOpChanged( + AppOpsManager.OP_CAMERA, + entry.getSbn().getUid(), + entry.getSbn().getPackageName(), + true); + + // THEN the app op still exists in the notification entry + Assert.assertTrue(entry.mActiveAppOps.contains(AppOpsManager.OP_CAMERA)); + + // THEN notification views aren't updated since nothing changed + verify(mEntryManager, never()).updateNotifications(anyString()); + } + + @Test + public void testAppOps_appOpNotAddedToUnrelatedNotif() { + // GIVEN no notification entries correspond to the newly updated appOp + NotificationEntry entry = addFgEntry(); + when(mEntryManager.getPendingOrActiveNotif(entry.getKey())).thenReturn(null); + + // WHEN a new app op is detected + mFsc.onAppOpChanged( + AppOpsManager.OP_CAMERA, + entry.getSbn().getUid(), + entry.getSbn().getPackageName(), + true); + + // THEN we won't see appOps on the entry + Assert.assertFalse(entry.mActiveAppOps.contains(AppOpsManager.OP_CAMERA)); + + // THEN notification views aren't updated since nothing changed + verify(mEntryManager, never()).updateNotifications(anyString()); + } + + @Test public void testAppOpsCRUD() { // no crash on remove that doesn't exist mFsc.onAppOpChanged(9, 1000, "pkg1", false); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java index 80fa8cc7d931..92a2c8738344 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java @@ -211,6 +211,19 @@ public class NotificationViewHierarchyManagerTest extends SysuiTestCase { } @Test + public void testUpdateNotificationViews_appOps() throws Exception { + NotificationEntry entry0 = createEntry(); + entry0.setRow(spy(entry0.getRow())); + when(mEntryManager.getVisibleNotifications()).thenReturn( + Lists.newArrayList(entry0)); + mListContainer.addContainerView(entry0.getRow()); + + mViewHierarchyManager.updateNotificationViews(); + + verify(entry0.getRow(), times(1)).showAppOpsIcons(any()); + } + + @Test public void testReentrantCallsToOnDynamicPrivacyChangedPostForLater() { // GIVEN a ListContainer that will make a re-entrant call to updateNotificationViews() mMadeReentrantCall = false; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinatorTest.java index ae39035e8666..314b19140e7a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinatorTest.java @@ -72,6 +72,8 @@ public class AppOpsCoordinatorTest extends SysuiTestCase { private Notification mNotification; private AppOpsCoordinator mAppOpsCoordinator; private NotifFilter mForegroundFilter; + private NotifCollectionListener mNotifCollectionListener; + private AppOpsController.Callback mAppOpsCallback; private NotifLifetimeExtender mForegroundNotifLifetimeExtender; private FakeSystemClock mClock = new FakeSystemClock(); @@ -108,6 +110,18 @@ public class AppOpsCoordinatorTest extends SysuiTestCase { lifetimeExtenderCaptor.capture()); mForegroundNotifLifetimeExtender = lifetimeExtenderCaptor.getValue(); + // capture notifCollectionListener + ArgumentCaptor<NotifCollectionListener> notifCollectionCaptor = + ArgumentCaptor.forClass(NotifCollectionListener.class); + verify(mNotifPipeline, times(1)).addCollectionListener( + notifCollectionCaptor.capture()); + mNotifCollectionListener = notifCollectionCaptor.getValue(); + + // capture app ops callback + ArgumentCaptor<AppOpsController.Callback> appOpsCaptor = + ArgumentCaptor.forClass(AppOpsController.Callback.class); + verify(mAppOpsController).addCallback(any(int[].class), appOpsCaptor.capture()); + mAppOpsCallback = appOpsCaptor.getValue(); } @Test @@ -201,4 +215,134 @@ public class AppOpsCoordinatorTest extends SysuiTestCase { assertFalse(mForegroundNotifLifetimeExtender .shouldExtendLifetime(mEntry, NotificationListenerService.REASON_CLICK)); } + + @Test + public void testAppOpsUpdateOnlyAppliedToRelevantNotificationWithStandardLayout() { + // GIVEN three current notifications, two with the same key but from different users + NotificationEntry entry1 = new NotificationEntryBuilder() + .setUser(new UserHandle(NOTIF_USER_ID)) + .setPkg(TEST_PKG) + .setId(1) + .build(); + NotificationEntry entry2 = new NotificationEntryBuilder() + .setUser(new UserHandle(NOTIF_USER_ID)) + .setPkg(TEST_PKG) + .setId(2) + .build(); + NotificationEntry entry3_diffUser = new NotificationEntryBuilder() + .setUser(new UserHandle(NOTIF_USER_ID + 1)) + .setPkg(TEST_PKG) + .setId(2) + .build(); + when(mNotifPipeline.getAllNotifs()).thenReturn(List.of(entry1, entry2, entry3_diffUser)); + + // GIVEN that only entry2 has a standard layout + when(mForegroundServiceController.getStandardLayoutKeys(NOTIF_USER_ID, TEST_PKG)) + .thenReturn(new ArraySet<>(List.of(entry2.getKey()))); + + // WHEN a new app ops code comes in + mAppOpsCallback.onActiveStateChanged(47, NOTIF_USER_ID, TEST_PKG, true); + mExecutor.runAllReady(); + + // THEN entry2's app ops are updated, but no one else's are + assertEquals( + new ArraySet<>(), + entry1.mActiveAppOps); + assertEquals( + new ArraySet<>(List.of(47)), + entry2.mActiveAppOps); + assertEquals( + new ArraySet<>(), + entry3_diffUser.mActiveAppOps); + } + + @Test + public void testAppOpsUpdateAppliedToAllNotificationsWithStandardLayouts() { + // GIVEN three notifications with standard layouts + NotificationEntry entry1 = new NotificationEntryBuilder() + .setUser(new UserHandle(NOTIF_USER_ID)) + .setPkg(TEST_PKG) + .setId(1) + .build(); + NotificationEntry entry2 = new NotificationEntryBuilder() + .setUser(new UserHandle(NOTIF_USER_ID)) + .setPkg(TEST_PKG) + .setId(2) + .build(); + NotificationEntry entry3 = new NotificationEntryBuilder() + .setUser(new UserHandle(NOTIF_USER_ID)) + .setPkg(TEST_PKG) + .setId(3) + .build(); + when(mNotifPipeline.getAllNotifs()).thenReturn(List.of(entry1, entry2, entry3)); + when(mForegroundServiceController.getStandardLayoutKeys(NOTIF_USER_ID, TEST_PKG)) + .thenReturn(new ArraySet<>(List.of(entry1.getKey(), entry2.getKey(), + entry3.getKey()))); + + // WHEN a new app ops code comes in + mAppOpsCallback.onActiveStateChanged(47, NOTIF_USER_ID, TEST_PKG, true); + mExecutor.runAllReady(); + + // THEN all entries get updated + assertEquals( + new ArraySet<>(List.of(47)), + entry1.mActiveAppOps); + assertEquals( + new ArraySet<>(List.of(47)), + entry2.mActiveAppOps); + assertEquals( + new ArraySet<>(List.of(47)), + entry3.mActiveAppOps); + } + + @Test + public void testAppOpsAreRemoved() { + // GIVEN One notification which is associated with app ops + NotificationEntry entry = new NotificationEntryBuilder() + .setUser(new UserHandle(NOTIF_USER_ID)) + .setPkg(TEST_PKG) + .setId(2) + .build(); + when(mNotifPipeline.getAllNotifs()).thenReturn(List.of(entry)); + when(mForegroundServiceController.getStandardLayoutKeys(0, TEST_PKG)) + .thenReturn(new ArraySet<>(List.of(entry.getKey()))); + + // GIVEN that the notification's app ops are already [47, 33] + mAppOpsCallback.onActiveStateChanged(47, NOTIF_USER_ID, TEST_PKG, true); + mAppOpsCallback.onActiveStateChanged(33, NOTIF_USER_ID, TEST_PKG, true); + mExecutor.runAllReady(); + assertEquals( + new ArraySet<>(List.of(47, 33)), + entry.mActiveAppOps); + + // WHEN one of the app ops is removed + mAppOpsCallback.onActiveStateChanged(47, NOTIF_USER_ID, TEST_PKG, false); + mExecutor.runAllReady(); + + // THEN the entry's active app ops are updated as well + assertEquals( + new ArraySet<>(List.of(33)), + entry.mActiveAppOps); + } + + @Test + public void testNullAppOps() { + // GIVEN one notification with app ops + NotificationEntry entry = new NotificationEntryBuilder() + .setUser(new UserHandle(NOTIF_USER_ID)) + .setPkg(TEST_PKG) + .setId(2) + .build(); + entry.mActiveAppOps.clear(); + entry.mActiveAppOps.addAll(List.of(47, 33)); + + // WHEN the notification is updated and the foreground service controller returns null for + // this notification + when(mForegroundServiceController.getAppOps(entry.getSbn().getUser().getIdentifier(), + entry.getSbn().getPackageName())).thenReturn(null); + mNotifCollectionListener.onEntryUpdated(entry); + + // THEN the entry's active app ops is updated to empty + assertTrue(entry.mActiveAppOps.isEmpty()); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/AppOpsInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/AppOpsInfoTest.java new file mode 100644 index 000000000000..43d8b50bcf72 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/AppOpsInfoTest.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.statusbar.notification.row; + +import static android.app.AppOpsManager.OP_CAMERA; +import static android.app.AppOpsManager.OP_RECORD_AUDIO; +import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertTrue; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyBoolean; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Notification; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.os.UserHandle; +import android.service.notification.StatusBarNotification; +import android.test.suitebuilder.annotation.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.UiThreadTest; +import android.util.ArraySet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.internal.logging.testing.UiEventLoggerFake; +import com.android.systemui.R; +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.CountDownLatch; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@UiThreadTest +public class AppOpsInfoTest extends SysuiTestCase { + private static final String TEST_PACKAGE_NAME = "test_package"; + private static final int TEST_UID = 1; + + private AppOpsInfo mAppOpsInfo; + private final PackageManager mMockPackageManager = mock(PackageManager.class); + private final NotificationGuts mGutsParent = mock(NotificationGuts.class); + private StatusBarNotification mSbn; + private UiEventLoggerFake mUiEventLogger = new UiEventLoggerFake(); + + @Before + public void setUp() throws Exception { + // Inflate the layout + final LayoutInflater layoutInflater = LayoutInflater.from(mContext); + mAppOpsInfo = (AppOpsInfo) layoutInflater.inflate(R.layout.app_ops_info, null); + mAppOpsInfo.setGutsParent(mGutsParent); + + // PackageManager must return a packageInfo and applicationInfo. + final PackageInfo packageInfo = new PackageInfo(); + packageInfo.packageName = TEST_PACKAGE_NAME; + when(mMockPackageManager.getPackageInfo(eq(TEST_PACKAGE_NAME), anyInt())) + .thenReturn(packageInfo); + final ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.uid = TEST_UID; // non-zero + when(mMockPackageManager.getApplicationInfo(anyString(), anyInt())).thenReturn( + applicationInfo); + + mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, 0, + new Notification(), UserHandle.CURRENT, null, 0); + } + + @Test + public void testBindNotification_SetsTextApplicationName() { + when(mMockPackageManager.getApplicationLabel(any())).thenReturn("App Name"); + mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, new ArraySet<>()); + final TextView textView = mAppOpsInfo.findViewById(R.id.pkgname); + assertTrue(textView.getText().toString().contains("App Name")); + } + + @Test + public void testBindNotification_SetsPackageIcon() { + final Drawable iconDrawable = mock(Drawable.class); + when(mMockPackageManager.getApplicationIcon(any(ApplicationInfo.class))) + .thenReturn(iconDrawable); + mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, new ArraySet<>()); + final ImageView iconView = mAppOpsInfo.findViewById(R.id.pkgicon); + assertEquals(iconDrawable, iconView.getDrawable()); + } + + @Test + public void testBindNotification_SetsOnClickListenerForSettings() throws Exception { + ArraySet<Integer> expectedOps = new ArraySet<>(); + expectedOps.add(OP_CAMERA); + final CountDownLatch latch = new CountDownLatch(1); + mAppOpsInfo.bindGuts(mMockPackageManager, (View v, String pkg, int uid, + ArraySet<Integer> ops) -> { + assertEquals(TEST_PACKAGE_NAME, pkg); + assertEquals(expectedOps, ops); + assertEquals(TEST_UID, uid); + latch.countDown(); + }, mSbn, mUiEventLogger, expectedOps); + + final View settingsButton = mAppOpsInfo.findViewById(R.id.settings); + settingsButton.performClick(); + // Verify that listener was triggered. + assertEquals(0, latch.getCount()); + } + + @Test + public void testBindNotification_LogsOpen() throws Exception { + mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, new ArraySet<>()); + assertEquals(1, mUiEventLogger.numLogs()); + assertEquals(NotificationAppOpsEvent.NOTIFICATION_APP_OPS_OPEN.getId(), + mUiEventLogger.eventId(0)); + } + + @Test + public void testOk() { + ArraySet<Integer> expectedOps = new ArraySet<>(); + expectedOps.add(OP_CAMERA); + final CountDownLatch latch = new CountDownLatch(1); + mAppOpsInfo.bindGuts(mMockPackageManager, (View v, String pkg, int uid, + ArraySet<Integer> ops) -> { + assertEquals(TEST_PACKAGE_NAME, pkg); + assertEquals(expectedOps, ops); + assertEquals(TEST_UID, uid); + latch.countDown(); + }, mSbn, mUiEventLogger, expectedOps); + + final View okButton = mAppOpsInfo.findViewById(R.id.ok); + okButton.performClick(); + assertEquals(1, latch.getCount()); + verify(mGutsParent, times(1)).closeControls(eq(okButton), anyBoolean()); + } + + @Test + public void testPrompt_camera() { + ArraySet<Integer> expectedOps = new ArraySet<>(); + expectedOps.add(OP_CAMERA); + mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, expectedOps); + TextView prompt = mAppOpsInfo.findViewById(R.id.prompt); + assertEquals("This app is using the camera.", prompt.getText()); + } + + @Test + public void testPrompt_mic() { + ArraySet<Integer> expectedOps = new ArraySet<>(); + expectedOps.add(OP_RECORD_AUDIO); + mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, expectedOps); + TextView prompt = mAppOpsInfo.findViewById(R.id.prompt); + assertEquals("This app is using the microphone.", prompt.getText()); + } + + @Test + public void testPrompt_overlay() { + ArraySet<Integer> expectedOps = new ArraySet<>(); + expectedOps.add(OP_SYSTEM_ALERT_WINDOW); + mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, expectedOps); + TextView prompt = mAppOpsInfo.findViewById(R.id.prompt); + assertEquals("This app is displaying over other apps on your screen.", prompt.getText()); + } + + @Test + public void testPrompt_camera_mic() { + ArraySet<Integer> expectedOps = new ArraySet<>(); + expectedOps.add(OP_CAMERA); + expectedOps.add(OP_RECORD_AUDIO); + mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, expectedOps); + TextView prompt = mAppOpsInfo.findViewById(R.id.prompt); + assertEquals("This app is using the microphone and camera.", prompt.getText()); + } + + @Test + public void testPrompt_camera_mic_overlay() { + ArraySet<Integer> expectedOps = new ArraySet<>(); + expectedOps.add(OP_CAMERA); + expectedOps.add(OP_RECORD_AUDIO); + expectedOps.add(OP_SYSTEM_ALERT_WINDOW); + mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, expectedOps); + TextView prompt = mAppOpsInfo.findViewById(R.id.prompt); + assertEquals("This app is displaying over other apps on your screen and using" + + " the microphone and camera.", prompt.getText()); + } + + @Test + public void testPrompt_camera_overlay() { + ArraySet<Integer> expectedOps = new ArraySet<>(); + expectedOps.add(OP_CAMERA); + expectedOps.add(OP_SYSTEM_ALERT_WINDOW); + mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, expectedOps); + TextView prompt = mAppOpsInfo.findViewById(R.id.prompt); + assertEquals("This app is displaying over other apps on your screen and using" + + " the camera.", prompt.getText()); + } + + @Test + public void testPrompt_mic_overlay() { + ArraySet<Integer> expectedOps = new ArraySet<>(); + expectedOps.add(OP_RECORD_AUDIO); + expectedOps.add(OP_SYSTEM_ALERT_WINDOW); + mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, expectedOps); + TextView prompt = mAppOpsInfo.findViewById(R.id.prompt); + assertEquals("This app is displaying over other apps on your screen and using" + + " the microphone.", prompt.getText()); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java index 4758d2318889..2684cc29aa93 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java @@ -35,10 +35,12 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.AppOpsManager; import android.app.NotificationChannel; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; +import android.util.ArraySet; import android.view.View; import androidx.test.filters.SmallTest; @@ -211,6 +213,46 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test + public void testShowAppOps_noHeader() { + // public notification is custom layout - no header + mGroupRow.setSensitive(true, true); + mGroupRow.setAppOpsOnClickListener(null); + mGroupRow.showAppOpsIcons(null); + } + + @Test + public void testShowAppOpsIcons_header() { + NotificationContentView publicLayout = mock(NotificationContentView.class); + mGroupRow.setPublicLayout(publicLayout); + NotificationContentView privateLayout = mock(NotificationContentView.class); + mGroupRow.setPrivateLayout(privateLayout); + NotificationChildrenContainer mockContainer = mock(NotificationChildrenContainer.class); + when(mockContainer.getNotificationChildCount()).thenReturn(1); + mGroupRow.setChildrenContainer(mockContainer); + + ArraySet<Integer> ops = new ArraySet<>(); + ops.add(AppOpsManager.OP_ANSWER_PHONE_CALLS); + mGroupRow.showAppOpsIcons(ops); + + verify(mockContainer, times(1)).showAppOpsIcons(ops); + verify(privateLayout, times(1)).showAppOpsIcons(ops); + verify(publicLayout, times(1)).showAppOpsIcons(ops); + + } + + @Test + public void testAppOpsOnClick() { + ExpandableNotificationRow.OnAppOpsClickListener l = mock( + ExpandableNotificationRow.OnAppOpsClickListener.class); + View view = mock(View.class); + + mGroupRow.setAppOpsOnClickListener(l); + + mGroupRow.getAppOpsOnClickListener().onClick(view); + verify(l, times(1)).onClick(any(), anyInt(), anyInt(), any()); + } + + @Test public void testHeadsUpAnimatingAwayListener() { mGroupRow.setHeadsUpAnimatingAway(true); Assert.assertEquals(true, mHeadsUpAnimatingAway); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java index b02f2746ce7a..ed4f8b330e23 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java @@ -76,6 +76,32 @@ public class NotificationContentViewTest extends SysuiTestCase { @Test @UiThreadTest + public void testShowAppOpsIcons() { + View mockContracted = mock(NotificationHeaderView.class); + when(mockContracted.findViewById(com.android.internal.R.id.mic)) + .thenReturn(mockContracted); + View mockExpanded = mock(NotificationHeaderView.class); + when(mockExpanded.findViewById(com.android.internal.R.id.mic)) + .thenReturn(mockExpanded); + View mockHeadsUp = mock(NotificationHeaderView.class); + when(mockHeadsUp.findViewById(com.android.internal.R.id.mic)) + .thenReturn(mockHeadsUp); + + mView.setContractedChild(mockContracted); + mView.setExpandedChild(mockExpanded); + mView.setHeadsUpChild(mockHeadsUp); + + ArraySet<Integer> ops = new ArraySet<>(); + ops.add(AppOpsManager.OP_RECORD_AUDIO); + mView.showAppOpsIcons(ops); + + verify(mockContracted, times(1)).setVisibility(View.VISIBLE); + verify(mockExpanded, times(1)).setVisibility(View.VISIBLE); + verify(mockHeadsUp, times(1)).setVisibility(View.VISIBLE); + } + + @Test + @UiThreadTest public void testExpandButtonFocusIsCalled() { View mockContractedEB = mock(NotificationExpandButton.class); View mockContracted = mock(NotificationHeaderView.class); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java index d2ff2ad8a684..0c6409b38d21 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java @@ -421,6 +421,7 @@ public class NotificationTestHelper { mBindStage, mock(OnExpandClickListener.class), mock(NotificationMediaManager.class), + mock(ExpandableNotificationRow.OnAppOpsClickListener.class), mock(FalsingManager.class), mStatusBarStateController, mPeopleNotificationIdentifier); |