DO NOT MERGE Revert "Remove app ops indicators from notifications"
This reverts commit 2778b62f1d60ada36655787fa716ee99346f7c68.
Reason for revert: these icons are still needed
Change-Id: I219af393f3d4cd08e431f38a3b66408e12f258bb
Bug: 163076432
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index b95a402..7622688 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -5151,10 +5151,18 @@
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 7a467d6..0c50cb7 100644
--- a/core/java/android/view/NotificationHeaderView.java
+++ b/core/java/android/view/NotificationHeaderView.java
@@ -52,11 +52,13 @@
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 @@
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 @@
// 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 @@
// 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 @@
}
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 @@
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 @@
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 @@
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 e3a456c..0791ed3 100644
--- a/core/java/com/android/internal/widget/ConversationLayout.java
+++ b/core/java/com/android/internal/widget/ConversationLayout.java
@@ -167,6 +167,8 @@
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 @@
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 @@
}
});
}
+ 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 03e130e..23b8bd3 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 7463d3f..8f24e79 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 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 5f88156..2deeb12 100644
--- a/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java
+++ b/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java
@@ -39,15 +39,21 @@
*/
@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 @@
}
/**
+ * 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 @@
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 1515272..bb44583 100644
--- a/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java
+++ b/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java
@@ -172,8 +172,24 @@
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 6b8afff..5bee9a7 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 @@
}
}
+ 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 84108b1..4b244bb 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 @@
// 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 @@
}
}
};
+
+ /**
+ * 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 0000000..28c53dc
--- /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 8ead7bf..94e12e8 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 @@
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 @@
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 @@
RowContentBindStage rowContentBindStage,
OnExpandClickListener onExpandClickListener,
NotificationMediaManager notificationMediaManager,
+ OnAppOpsClickListener onAppOpsClickListener,
FalsingManager falsingManager,
StatusBarStateController statusBarStateController,
PeopleNotificationIdentifier peopleNotificationIdentifier) {
@@ -1626,6 +1629,7 @@
mRowContentBindStage = rowContentBindStage;
mOnExpandClickListener = onExpandClickListener;
mMediaManager = notificationMediaManager;
+ setAppOpsOnClickListener(onAppOpsClickListener);
mFalsingManager = falsingManager;
mStatusbarStateController = statusBarStateController;
mPeopleNotificationIdentifier = peopleNotificationIdentifier;
@@ -1682,6 +1686,14 @@
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 @@
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 b132caf..7a6109d 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 @@
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 @@
mStatusBarStateController = statusBarStateController;
mNotificationGutsManager = notificationGutsManager;
mOnDismissRunnable = onDismissRunnable;
+ mOnAppOpsClickListener = mNotificationGutsManager::openGuts;
mAllowLongPress = allowLongPress;
mFalsingManager = falsingManager;
mPeopleNotificationIdentifier = peopleNotificationIdentifier;
@@ -120,6 +122,7 @@
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 41a9b18..1f5b063 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 @@
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 3eed18a..24883f5 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 @@
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 @@
}
/**
+ * 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 a167925..5e1e3b2 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 @@
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 @@
}
@Override
+ public MenuItem getAppOpsMenuItem(Context context) {
+ return mAppOpsItem;
+ }
+
+ @Override
public MenuItem getSnoozeMenuItem(Context context) {
return mSnoozeItem;
}
@@ -258,6 +264,7 @@
// 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 @@
mRightMenuItems.add(mSnoozeItem);
}
mRightMenuItems.add(mInfoItem);
+ mRightMenuItems.add(mAppOpsItem);
mLeftMenuItems.addAll(mRightMenuItems);
populateMenuViews();
@@ -680,6 +688,14 @@
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 86fc352..c747a7c 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 @@
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 @@
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 @@
}
}, TRANSFORMING_VIEW_TITLE);
resolveHeaderViews();
+ addAppOpsOnClickListener(row);
}
protected void resolveHeaderViews() {
@@ -113,6 +119,10 @@
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 @@
}
}
+ 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 @@
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 4bf2794..30080e3 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 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 5edcde1..99691b7 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 @@
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 e967a5d..60f0cd9 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 @@
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 @@
}
@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 80fa8cc..92a2c87 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 @@
}
@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 ae39035..314b191 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 @@
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 @@
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 @@
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 0000000..43d8b50
--- /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 4758d23..2684cc2 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.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 @@
}
@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 b02f274..ed4f8b3 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 @@
@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 d2ff2ad..0c6409b 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 @@
mBindStage,
mock(OnExpandClickListener.class),
mock(NotificationMediaManager.class),
+ mock(ExpandableNotificationRow.OnAppOpsClickListener.class),
mock(FalsingManager.class),
mStatusBarStateController,
mPeopleNotificationIdentifier);