Merge "Add package name when creating AudioTrack." into rvc-qpr-dev
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto
index 7d0d979..285eb4a 100644
--- a/cmds/statsd/src/atoms.proto
+++ b/cmds/statsd/src/atoms.proto
@@ -3863,6 +3863,17 @@
* system/core/lmkd/lmkd.c
*/
message LmkKillOccurred {
+ enum Reason {
+ UNKNOWN = 0;
+ PRESSURE_AFTER_KILL = 1;
+ NOT_RESPONDING = 2;
+ LOW_SWAP_AND_THRASHING = 3;
+ LOW_MEM_AND_SWAP = 4;
+ LOW_MEM_AND_THRASHING = 5;
+ DIRECT_RECL_AND_THRASHING = 6;
+ LOW_MEM_AND_SWAP_UTIL = 7;
+ }
+
// The uid if available. -1 means not available.
optional int32 uid = 1 [(is_uid) = true];
@@ -3892,6 +3903,15 @@
// Min oom adj score considered by lmkd.
optional int32 min_oom_score = 10;
+
+ // Free physical memory on device at LMK time.
+ optional int32 free_mem_kb = 11;
+
+ // Free swap on device at LMK time.
+ optional int32 free_swap_kb = 12;
+
+ // What triggered the LMK event.
+ optional Reason reason = 13;
}
/*
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index af36260..c273cf08 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/CarSystemUI/res/values/config.xml b/packages/CarSystemUI/res/values/config.xml
index 039f2c0..24157f9 100644
--- a/packages/CarSystemUI/res/values/config.xml
+++ b/packages/CarSystemUI/res/values/config.xml
@@ -53,6 +53,16 @@
<integer name="config_rightSystemBarZOrder">0</integer>
<integer name="config_bottomSystemBarZOrder">10</integer>
+ <!-- If set to true, the corresponding system bar will be hidden when Keyboard (IME) appears.
+ NOTE: hideBottomSystemBarKeyboard must not be overlaid directly here. To change its value,
+ overlay config_automotiveHideNavBarForKeyboard in framework/base/core/res/res. -->
+ <bool name="config_hideTopSystemBarForKeyboard">false</bool>
+ <bool name="config_hideBottomSystemBarForKeyboard">
+ @*android:bool/config_automotiveHideNavBarForKeyboard
+ </bool>
+ <bool name="config_hideLeftSystemBarForKeyboard">false</bool>
+ <bool name="config_hideRightSystemBarForKeyboard">false</bool>
+
<!-- Disable normal notification rendering; we handle that ourselves -->
<bool name="config_renderNotifications">false</bool>
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/CarNavigationBar.java b/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/CarNavigationBar.java
index 9584850..b6d251f 100644
--- a/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/CarNavigationBar.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/CarNavigationBar.java
@@ -91,7 +91,11 @@
private ActivityManagerWrapper mActivityManagerWrapper;
// If the nav bar should be hidden when the soft keyboard is visible.
- private boolean mHideNavBarForKeyboard;
+ private boolean mHideTopBarForKeyboard;
+ private boolean mHideLeftBarForKeyboard;
+ private boolean mHideRightBarForKeyboard;
+ private boolean mHideBottomBarForKeyboard;
+
private boolean mBottomNavBarVisible;
// Nav bar views.
@@ -160,8 +164,13 @@
@Override
public void start() {
// Set initial state.
- mHideNavBarForKeyboard = mResources.getBoolean(
- com.android.internal.R.bool.config_automotiveHideNavBarForKeyboard);
+ mHideTopBarForKeyboard = mSystemBarConfigs.getHideForKeyboardBySide(SystemBarConfigs.TOP);
+ mHideBottomBarForKeyboard = mSystemBarConfigs.getHideForKeyboardBySide(
+ SystemBarConfigs.BOTTOM);
+ mHideLeftBarForKeyboard = mSystemBarConfigs.getHideForKeyboardBySide(SystemBarConfigs.LEFT);
+ mHideRightBarForKeyboard = mSystemBarConfigs.getHideForKeyboardBySide(
+ SystemBarConfigs.RIGHT);
+
mBottomNavBarVisible = false;
// Connect into the status bar manager service
@@ -407,17 +416,30 @@
@Override
public void setImeWindowStatus(int displayId, IBinder token, int vis, int backDisposition,
boolean showImeSwitcher) {
- if (!mHideNavBarForKeyboard) {
- return;
- }
-
if (mContext.getDisplayId() != displayId) {
return;
}
boolean isKeyboardVisible = (vis & InputMethodService.IME_VISIBLE) != 0;
- mCarNavigationBarController.setBottomWindowVisibility(
- isKeyboardVisible ? View.GONE : View.VISIBLE);
+
+ if (mHideTopBarForKeyboard) {
+ mCarNavigationBarController.setTopWindowVisibility(
+ isKeyboardVisible ? View.GONE : View.VISIBLE);
+ }
+
+ if (mHideBottomBarForKeyboard) {
+ mCarNavigationBarController.setBottomWindowVisibility(
+ isKeyboardVisible ? View.GONE : View.VISIBLE);
+ }
+
+ if (mHideLeftBarForKeyboard) {
+ mCarNavigationBarController.setLeftWindowVisibility(
+ isKeyboardVisible ? View.GONE : View.VISIBLE);
+ }
+ if (mHideRightBarForKeyboard) {
+ mCarNavigationBarController.setRightWindowVisibility(
+ isKeyboardVisible ? View.GONE : View.VISIBLE);
+ }
}
@Override
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/CarNavigationBarController.java b/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/CarNavigationBarController.java
index fe26040..e522d19 100644
--- a/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/CarNavigationBarController.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/CarNavigationBarController.java
@@ -79,9 +79,7 @@
* Hides all system bars.
*/
public void hideBars() {
- if (mTopView != null) {
- mTopView.setVisibility(View.GONE);
- }
+ setTopWindowVisibility(View.GONE);
setBottomWindowVisibility(View.GONE);
setLeftWindowVisibility(View.GONE);
setRightWindowVisibility(View.GONE);
@@ -91,9 +89,7 @@
* Shows all system bars.
*/
public void showBars() {
- if (mTopView != null) {
- mTopView.setVisibility(View.VISIBLE);
- }
+ setTopWindowVisibility(View.VISIBLE);
setBottomWindowVisibility(View.VISIBLE);
setLeftWindowVisibility(View.VISIBLE);
setRightWindowVisibility(View.VISIBLE);
@@ -135,6 +131,11 @@
return mShowRight ? mNavigationBarViewFactory.getRightWindow() : null;
}
+ /** Toggles the top nav bar visibility. */
+ public boolean setTopWindowVisibility(@View.Visibility int visibility) {
+ return setWindowVisibility(getTopWindow(), visibility);
+ }
+
/** Toggles the bottom nav bar visibility. */
public boolean setBottomWindowVisibility(@View.Visibility int visibility) {
return setWindowVisibility(getBottomWindow(), visibility);
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/SystemBarConfigs.java b/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/SystemBarConfigs.java
index e7d3194..694ae6d9 100644
--- a/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/SystemBarConfigs.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/SystemBarConfigs.java
@@ -98,6 +98,7 @@
readConfigs();
checkEnabledBarsHaveUniqueBarTypes();
checkSystemBarEnabledForNotificationPanel();
+ checkHideBottomBarForKeyboardConfigSync();
setInsetPaddingsForOverlappingCorners();
sortSystemBarSidesByZOrder();
}
@@ -122,6 +123,11 @@
}
}
+ protected boolean getHideForKeyboardBySide(@SystemBarSide int side) {
+ return mSystemBarConfigMap.get(side) != null
+ && mSystemBarConfigMap.get(side).getHideForKeyboard();
+ }
+
protected void insetSystemBar(@SystemBarSide int side, CarNavigationBarView view) {
int[] paddings = mSystemBarConfigMap.get(side).getPaddings();
view.setPadding(paddings[2], paddings[0], paddings[3], paddings[1]);
@@ -167,6 +173,8 @@
com.android.internal.R.dimen.status_bar_height))
.setBarType(mResources.getInteger(R.integer.config_topSystemBarType))
.setZOrder(mResources.getInteger(R.integer.config_topSystemBarZOrder))
+ .setHideForKeyboard(mResources.getBoolean(
+ R.bool.config_hideTopSystemBarForKeyboard))
.build();
mSystemBarConfigMap.put(TOP, topBarConfig);
}
@@ -180,6 +188,8 @@
.setBarType(mResources.getInteger(R.integer.config_bottomSystemBarType))
.setZOrder(
mResources.getInteger(R.integer.config_bottomSystemBarZOrder))
+ .setHideForKeyboard(mResources.getBoolean(
+ R.bool.config_hideBottomSystemBarForKeyboard))
.build();
mSystemBarConfigMap.put(BOTTOM, bottomBarConfig);
}
@@ -192,6 +202,8 @@
R.dimen.car_left_navigation_bar_width))
.setBarType(mResources.getInteger(R.integer.config_leftSystemBarType))
.setZOrder(mResources.getInteger(R.integer.config_leftSystemBarZOrder))
+ .setHideForKeyboard(mResources.getBoolean(
+ R.bool.config_hideLeftSystemBarForKeyboard))
.build();
mSystemBarConfigMap.put(LEFT, leftBarConfig);
}
@@ -204,6 +216,8 @@
R.dimen.car_right_navigation_bar_width))
.setBarType(mResources.getInteger(R.integer.config_rightSystemBarType))
.setZOrder(mResources.getInteger(R.integer.config_rightSystemBarZOrder))
+ .setHideForKeyboard(mResources.getBoolean(
+ R.bool.config_hideRightSystemBarForKeyboard))
.build();
mSystemBarConfigMap.put(RIGHT, rightBarConfig);
}
@@ -239,19 +253,37 @@
e.printStackTrace();
}
- if (!mTopNavBarEnabled && notificationPanelMediatorUsed.isAssignableFrom(
- TopNotificationPanelViewMediator.class)) {
+ if (!mTopNavBarEnabled && TopNotificationPanelViewMediator.class.isAssignableFrom(
+ notificationPanelMediatorUsed)) {
throw new RuntimeException(
"Top System Bar must be enabled to use " + notificationPanelMediatorName);
}
- if (!mBottomNavBarEnabled && notificationPanelMediatorUsed.isAssignableFrom(
- BottomNotificationPanelViewMediator.class)) {
+ if (!mBottomNavBarEnabled && BottomNotificationPanelViewMediator.class.isAssignableFrom(
+ notificationPanelMediatorUsed)) {
throw new RuntimeException("Bottom System Bar must be enabled to use "
+ notificationPanelMediatorName);
}
}
+ private void checkHideBottomBarForKeyboardConfigSync() throws RuntimeException {
+ if (mBottomNavBarEnabled) {
+ boolean actual = mResources.getBoolean(R.bool.config_hideBottomSystemBarForKeyboard);
+ boolean expected = mResources.getBoolean(
+ com.android.internal.R.bool.config_automotiveHideNavBarForKeyboard);
+
+ if (actual != expected) {
+ throw new RuntimeException("config_hideBottomSystemBarForKeyboard must not be "
+ + "overlaid directly and should always refer to"
+ + "config_automotiveHideNavBarForKeyboard. However, their values "
+ + "currently do not sync. Set config_hideBottomSystemBarForKeyguard to "
+ + "@*android:bool/config_automotiveHideNavBarForKeyboard. To change its "
+ + "value, overlay config_automotiveHideNavBarForKeyboard in "
+ + "framework/base/core/res/res.");
+ }
+ }
+ }
+
private void setInsetPaddingsForOverlappingCorners() {
setInsetPaddingForOverlappingCorner(TOP, LEFT);
setInsetPaddingForOverlappingCorner(TOP, RIGHT);
@@ -320,14 +352,17 @@
private final int mBarType;
private final int mGirth;
private final int mZOrder;
+ private final boolean mHideForKeyboard;
private int[] mPaddings = new int[]{0, 0, 0, 0};
- private SystemBarConfig(@SystemBarSide int side, int barType, int girth, int zOrder) {
+ private SystemBarConfig(@SystemBarSide int side, int barType, int girth, int zOrder,
+ boolean hideForKeyboard) {
mSide = side;
mBarType = barType;
mGirth = girth;
mZOrder = zOrder;
+ mHideForKeyboard = hideForKeyboard;
}
private int getSide() {
@@ -346,6 +381,10 @@
return mZOrder;
}
+ private boolean getHideForKeyboard() {
+ return mHideForKeyboard;
+ }
+
private int[] getPaddings() {
return mPaddings;
}
@@ -383,6 +422,7 @@
private int mBarType;
private int mGirth;
private int mZOrder;
+ private boolean mHideForKeyboard;
private SystemBarConfigBuilder setSide(@SystemBarSide int side) {
mSide = side;
@@ -404,8 +444,13 @@
return this;
}
+ private SystemBarConfigBuilder setHideForKeyboard(boolean hide) {
+ mHideForKeyboard = hide;
+ return this;
+ }
+
private SystemBarConfig build() {
- return new SystemBarConfig(mSide, mBarType, mGirth, mZOrder);
+ return new SystemBarConfig(mSide, mBarType, mGirth, mZOrder, mHideForKeyboard);
}
}
}
diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/car/navigationbar/CarNavigationBarControllerTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/car/navigationbar/CarNavigationBarControllerTest.java
index 84c8404..3fd0852 100644
--- a/packages/CarSystemUI/tests/src/com/android/systemui/car/navigationbar/CarNavigationBarControllerTest.java
+++ b/packages/CarSystemUI/tests/src/com/android/systemui/car/navigationbar/CarNavigationBarControllerTest.java
@@ -251,6 +251,28 @@
}
@Test
+ public void testSetTopWindowVisibility_setTrue_isVisible() {
+ mTestableResources.addOverride(R.bool.config_enableTopNavigationBar, true);
+ mCarNavigationBar = createNavigationBarController();
+
+ ViewGroup window = mCarNavigationBar.getTopWindow();
+ mCarNavigationBar.setTopWindowVisibility(View.VISIBLE);
+
+ assertThat(window.getVisibility()).isEqualTo(View.VISIBLE);
+ }
+
+ @Test
+ public void testSetTopWindowVisibility_setFalse_isGone() {
+ mTestableResources.addOverride(R.bool.config_enableTopNavigationBar, true);
+ mCarNavigationBar = createNavigationBarController();
+
+ ViewGroup window = mCarNavigationBar.getTopWindow();
+ mCarNavigationBar.setTopWindowVisibility(View.GONE);
+
+ assertThat(window.getVisibility()).isEqualTo(View.GONE);
+ }
+
+ @Test
public void testSetBottomWindowVisibility_setTrue_isVisible() {
mTestableResources.addOverride(R.bool.config_enableBottomNavigationBar, true);
mCarNavigationBar = createNavigationBarController();
diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/car/navigationbar/SystemBarConfigsTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/car/navigationbar/SystemBarConfigsTest.java
index 8b15899..96f0504 100644
--- a/packages/CarSystemUI/tests/src/com/android/systemui/car/navigationbar/SystemBarConfigsTest.java
+++ b/packages/CarSystemUI/tests/src/com/android/systemui/car/navigationbar/SystemBarConfigsTest.java
@@ -17,6 +17,7 @@
package com.android.systemui.car.navigationbar;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@@ -31,7 +32,14 @@
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.car.CarDeviceProvisionedController;
import com.android.systemui.car.CarSystemUiTest;
+import com.android.systemui.car.notification.NotificationPanelViewController;
+import com.android.systemui.car.notification.NotificationPanelViewMediator;
+import com.android.systemui.car.notification.PowerManagerHelper;
+import com.android.systemui.car.notification.TopNotificationPanelViewMediator;
+import com.android.systemui.statusbar.policy.ConfigurationController;
import org.junit.Before;
import org.junit.Test;
@@ -92,6 +100,43 @@
mSystemBarConfigs = new SystemBarConfigs(mResources);
}
+ @Test(expected = RuntimeException.class)
+ public void onInit_hideBottomSystemBarForKeyboardValueDoNotSync_throwsRuntimeException() {
+ when(mResources.getBoolean(R.bool.config_hideBottomSystemBarForKeyboard)).thenReturn(false);
+ when(mResources.getBoolean(
+ com.android.internal.R.bool.config_automotiveHideNavBarForKeyboard)).thenReturn(
+ true);
+
+ mSystemBarConfigs = new SystemBarConfigs(mResources);
+ }
+
+ @Test
+ public void onInit_topNotifPanelViewMediatorUsed_topBarEnabled_doesNotThrowException() {
+ when(mResources.getBoolean(R.bool.config_enableTopNavigationBar)).thenReturn(true);
+ when(mResources.getString(R.string.config_notificationPanelViewMediator)).thenReturn(
+ TestTopNotificationPanelViewMediator.class.getName());
+
+ mSystemBarConfigs = new SystemBarConfigs(mResources);
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void onInit_topNotifPanelViewMediatorUsed_topBarNotEnabled_throwsRuntimeException() {
+ when(mResources.getBoolean(R.bool.config_enableTopNavigationBar)).thenReturn(false);
+ when(mResources.getString(R.string.config_notificationPanelViewMediator)).thenReturn(
+ TestTopNotificationPanelViewMediator.class.getName());
+
+ mSystemBarConfigs = new SystemBarConfigs(mResources);
+ }
+
+ @Test
+ public void onInit_notificationPanelViewMediatorUsed_topBarNotEnabled_doesNotThrowException() {
+ when(mResources.getBoolean(R.bool.config_enableTopNavigationBar)).thenReturn(false);
+ when(mResources.getString(R.string.config_notificationPanelViewMediator)).thenReturn(
+ NotificationPanelViewMediator.class.getName());
+
+ mSystemBarConfigs = new SystemBarConfigs(mResources);
+ }
+
@Test
public void getTopSystemBarLayoutParams_topBarEnabled_returnsTopSystemBarLayoutParams() {
mSystemBarConfigs = new SystemBarConfigs(mResources);
@@ -112,6 +157,26 @@
}
@Test
+ public void getTopSystemBarHideForKeyboard_hideBarForKeyboard_returnsTrue() {
+ when(mResources.getBoolean(R.bool.config_hideTopSystemBarForKeyboard)).thenReturn(true);
+ mSystemBarConfigs = new SystemBarConfigs(mResources);
+
+ boolean hideKeyboard = mSystemBarConfigs.getHideForKeyboardBySide(SystemBarConfigs.TOP);
+
+ assertTrue(hideKeyboard);
+ }
+
+ @Test
+ public void getTopSystemBarHideForKeyboard_topBarNotEnabled_returnsFalse() {
+ when(mResources.getBoolean(R.bool.config_enableTopNavigationBar)).thenReturn(false);
+ mSystemBarConfigs = new SystemBarConfigs(mResources);
+
+ boolean hideKeyboard = mSystemBarConfigs.getHideForKeyboardBySide(SystemBarConfigs.TOP);
+
+ assertFalse(hideKeyboard);
+ }
+
+ @Test
public void topSystemBarHasHigherZOrderThanHuns_topSystemBarIsNavigationBarPanelType() {
when(mResources.getInteger(R.integer.config_topSystemBarZOrder)).thenReturn(
SystemBarConfigs.getHunZOrder() + 1);
@@ -158,5 +223,30 @@
when(mResources.getInteger(R.integer.config_bottomSystemBarZOrder)).thenReturn(10);
when(mResources.getInteger(R.integer.config_leftSystemBarZOrder)).thenReturn(2);
when(mResources.getInteger(R.integer.config_rightSystemBarZOrder)).thenReturn(3);
+
+ when(mResources.getBoolean(R.bool.config_hideTopSystemBarForKeyboard)).thenReturn(false);
+ when(mResources.getBoolean(
+ com.android.internal.R.bool.config_automotiveHideNavBarForKeyboard)).thenReturn(
+ false);
+ when(mResources.getBoolean(R.bool.config_hideLeftSystemBarForKeyboard)).thenReturn(
+ false);
+ when(mResources.getBoolean(R.bool.config_hideRightSystemBarForKeyboard)).thenReturn(
+ false);
+ }
+
+ // Intentionally using a subclass of TopNotificationPanelViewMediator for testing purposes to
+ // ensure that OEM's will be able to implement and use their own NotificationPanelViewMediator.
+ private class TestTopNotificationPanelViewMediator extends
+ TopNotificationPanelViewMediator {
+ TestTopNotificationPanelViewMediator(
+ CarNavigationBarController carNavigationBarController,
+ NotificationPanelViewController notificationPanelViewController,
+ PowerManagerHelper powerManagerHelper,
+ BroadcastDispatcher broadcastDispatcher,
+ CarDeviceProvisionedController carDeviceProvisionedController,
+ ConfigurationController configurationController) {
+ super(carNavigationBarController, notificationPanelViewController, powerManagerHelper,
+ broadcastDispatcher, carDeviceProvisionedController, configurationController);
+ }
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
index 9d06c84..72a6074 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
@@ -465,7 +465,16 @@
synchronized (mMediaDevicesLock) {
mMediaDevices.clear();
mMediaDevices.addAll(devices);
- mMediaDevices.addAll(buildDisconnectedBluetoothDevice());
+ // Add disconnected bluetooth devices only when phone output device is available.
+ for (MediaDevice device : devices) {
+ final int type = device.getDeviceType();
+ if (type == MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE
+ || type == MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE
+ || type == MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE) {
+ mMediaDevices.addAll(buildDisconnectedBluetoothDevice());
+ break;
+ }
+ }
}
final MediaDevice infoMediaDevice = mInfoMediaManager.getCurrentConnectedDevice();
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
index a654fd4..8e850b2 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
@@ -585,6 +585,7 @@
when(device1.getId()).thenReturn(TEST_DEVICE_ID_1);
when(device2.getId()).thenReturn(TEST_DEVICE_ID_2);
when(device3.getId()).thenReturn(TEST_DEVICE_ID_3);
+ when(device1.getDeviceType()).thenReturn(MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE);
when(mLocalMediaManager.mPhoneDevice.getId()).thenReturn("test_phone_id");
assertThat(mLocalMediaManager.mMediaDevices).hasSize(2);
@@ -683,6 +684,7 @@
when(device1.getId()).thenReturn(TEST_DEVICE_ID_1);
when(device2.getId()).thenReturn(TEST_DEVICE_ID_2);
when(device3.getId()).thenReturn(TEST_DEVICE_ID_3);
+ when(device1.getDeviceType()).thenReturn(MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE);
when(mLocalMediaManager.mPhoneDevice.getId()).thenReturn("test_phone_id");
assertThat(mLocalMediaManager.mMediaDevices).hasSize(2);
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/media/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt
index 662831e..24ca970 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt
@@ -27,6 +27,7 @@
import javax.inject.Singleton
private const val TAG = "MediaDataFilter"
+private const val DEBUG = true
/**
* Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user
@@ -98,7 +99,7 @@
// are up to date
mediaEntries.clear()
keyCopy.forEach {
- Log.d(TAG, "Removing $it after user change")
+ if (DEBUG) Log.d(TAG, "Removing $it after user change")
listenersCopy.forEach { listener ->
listener.onMediaDataRemoved(it)
}
@@ -106,7 +107,7 @@
dataSource.getData().forEach { (key, data) ->
if (lockscreenUserManager.isCurrentProfile(data.userId)) {
- Log.d(TAG, "Re-adding $key after user change")
+ if (DEBUG) Log.d(TAG, "Re-adding $key after user change")
mediaEntries.put(key, data)
listenersCopy.forEach { listener ->
listener.onMediaDataLoaded(key, null, data)
@@ -119,6 +120,7 @@
* Invoked when the user has dismissed the media carousel
*/
fun onSwipeToDismiss() {
+ if (DEBUG) Log.d(TAG, "Media carousel swiped away")
val mediaKeys = mediaEntries.keys.toSet()
mediaKeys.forEach {
mediaDataManager.setTimedOut(it, timedOut = true)
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
index 7e246c8..b85e365 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
@@ -67,6 +67,7 @@
)
private const val TAG = "MediaDataManager"
+private const val DEBUG = true
private const val DEFAULT_LUMINOSITY = 0.25f
private const val LUMINOSITY_THRESHOLD = 0.05f
private const val SATURATION_MULTIPLIER = 0.8f
@@ -265,7 +266,7 @@
fun removeListener(listener: Listener) = listeners.remove(listener)
/**
- * Called whenever the player has been paused or stopped for a while.
+ * Called whenever the player has been paused or stopped for a while, or swiped from QQS.
* This will make the player not active anymore, hiding it from QQS and Keyguard.
* @see MediaData.active
*/
@@ -275,6 +276,7 @@
return
}
it.active = !timedOut
+ if (DEBUG) Log.d(TAG, "Updating $token timedOut: $timedOut")
onMediaDataLoaded(token, token, it)
}
}
@@ -307,7 +309,9 @@
return
}
- Log.d(TAG, "adding track for $userId from browser: $desc")
+ if (DEBUG) {
+ Log.d(TAG, "adding track for $userId from browser: $desc")
+ }
// Album art
var artworkBitmap = desc.iconBitmap
@@ -408,7 +412,7 @@
if (actions != null) {
for ((index, action) in actions.withIndex()) {
if (action.getIcon() == null) {
- Log.i(TAG, "No icon for action $index ${action.title}")
+ if (DEBUG) Log.i(TAG, "No icon for action $index ${action.title}")
actionsToShowCollapsed.remove(index)
continue
}
@@ -455,7 +459,7 @@
if (!TextUtils.isEmpty(uriString)) {
val albumArt = loadBitmapFromUri(Uri.parse(uriString))
if (albumArt != null) {
- Log.d(TAG, "loaded art from $uri")
+ if (DEBUG) Log.d(TAG, "loaded art from $uri")
return albumArt
}
}
@@ -546,7 +550,7 @@
val removed = mediaEntries.remove(key)
if (useMediaResumption && removed?.resumeAction != null &&
!isBlockedFromResume(removed?.packageName)) {
- Log.d(TAG, "Not removing $key because resumable")
+ if (DEBUG) Log.d(TAG, "Not removing $key because resumable")
// Move to resume key (aka package name) if that key doesn't already exist.
val resumeAction = getResumeMediaAction(removed.resumeAction!!)
val updated = removed.copy(token = null, actions = listOf(resumeAction),
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
index 143f849..ae7f66b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
@@ -19,8 +19,12 @@
import android.content.Context
import android.media.MediaRouter2Manager
import android.media.session.MediaController
+import androidx.annotation.AnyThread
+import androidx.annotation.MainThread
+import androidx.annotation.WorkerThread
import com.android.settingslib.media.LocalMediaManager
import com.android.settingslib.media.MediaDevice
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.Dumpable
import com.android.systemui.dump.DumpManager
@@ -39,11 +43,12 @@
private val localMediaManagerFactory: LocalMediaManagerFactory,
private val mr2manager: MediaRouter2Manager,
@Main private val fgExecutor: Executor,
+ @Background private val bgExecutor: Executor,
private val mediaDataManager: MediaDataManager,
private val dumpManager: DumpManager
) : MediaDataManager.Listener, Dumpable {
private val listeners: MutableSet<Listener> = mutableSetOf()
- private val entries: MutableMap<String, Token> = mutableMapOf()
+ private val entries: MutableMap<String, Entry> = mutableMapOf()
init {
mediaDataManager.addListener(this)
@@ -71,7 +76,7 @@
val controller = data.token?.let {
MediaController(context, it)
}
- entry = Token(key, oldKey, controller,
+ entry = Entry(key, oldKey, controller,
localMediaManagerFactory.create(data.packageName))
entries[key] = entry
entry.start()
@@ -99,6 +104,7 @@
}
}
+ @MainThread
private fun processDevice(key: String, oldKey: String?, device: MediaDevice?) {
val enabled = device != null
val data = MediaDeviceData(enabled, device?.iconWithoutBackground, device?.name)
@@ -114,12 +120,13 @@
fun onKeyRemoved(key: String)
}
- private inner class Token(
+ private inner class Entry(
val key: String,
val oldKey: String?,
val controller: MediaController?,
val localMediaManager: LocalMediaManager
) : LocalMediaManager.DeviceCallback {
+
val token
get() = controller?.sessionToken
private var started = false
@@ -127,20 +134,27 @@
set(value) {
if (!started || value != field) {
field = value
- processDevice(key, oldKey, value)
+ fgExecutor.execute {
+ processDevice(key, oldKey, value)
+ }
}
}
- fun start() {
+
+ @AnyThread
+ fun start() = bgExecutor.execute {
localMediaManager.registerCallback(this)
localMediaManager.startScan()
updateCurrent()
started = true
}
- fun stop() {
+
+ @AnyThread
+ fun stop() = bgExecutor.execute {
started = false
localMediaManager.stopScan()
localMediaManager.unregisterCallback(this)
}
+
fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) {
val route = controller?.let {
mr2manager.getRoutingSessionForMediaController(it)
@@ -152,14 +166,18 @@
println(" route=$route")
}
}
- override fun onDeviceListUpdate(devices: List<MediaDevice>?) = fgExecutor.execute {
+
+ override fun onDeviceListUpdate(devices: List<MediaDevice>?) = bgExecutor.execute {
updateCurrent()
}
+
override fun onSelectedDeviceStateChanged(device: MediaDevice, state: Int) {
- fgExecutor.execute {
+ bgExecutor.execute {
updateCurrent()
}
}
+
+ @WorkerThread
private fun updateCurrent() {
val device = localMediaManager.getCurrentConnectedDevice()
controller?.let {
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
index acc7f81..8ec3db5 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
@@ -41,6 +41,7 @@
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.LongRunning;
import com.android.systemui.settings.CurrentUserContextTracker;
+import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
import java.io.IOException;
import java.util.concurrent.Executor;
@@ -70,7 +71,7 @@
private static final String ACTION_SHARE = "com.android.systemui.screenrecord.SHARE";
private final RecordingController mController;
-
+ private final KeyguardDismissUtil mKeyguardDismissUtil;
private ScreenRecordingAudioSource mAudioSource;
private boolean mShowTaps;
private boolean mOriginalShowTaps;
@@ -83,12 +84,13 @@
@Inject
public RecordingService(RecordingController controller, @LongRunning Executor executor,
UiEventLogger uiEventLogger, NotificationManager notificationManager,
- CurrentUserContextTracker userContextTracker) {
+ CurrentUserContextTracker userContextTracker, KeyguardDismissUtil keyguardDismissUtil) {
mController = controller;
mLongExecutor = executor;
mUiEventLogger = uiEventLogger;
mNotificationManager = notificationManager;
mUserContextTracker = userContextTracker;
+ mKeyguardDismissUtil = keyguardDismissUtil;
}
/**
@@ -168,16 +170,17 @@
Intent shareIntent = new Intent(Intent.ACTION_SEND)
.setType("video/mp4")
.putExtra(Intent.EXTRA_STREAM, shareUri);
- String shareLabel = getResources().getString(R.string.screenrecord_share_label);
+ mKeyguardDismissUtil.executeWhenUnlocked(() -> {
+ String shareLabel = getResources().getString(R.string.screenrecord_share_label);
+ startActivity(Intent.createChooser(shareIntent, shareLabel)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ // Remove notification
+ mNotificationManager.cancelAsUser(null, NOTIFICATION_VIEW_ID, currentUser);
+ return false;
+ }, false);
// Close quick shade
sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
-
- // Remove notification
- mNotificationManager.cancelAsUser(null, NOTIFICATION_VIEW_ID, currentUser);
-
- startActivity(Intent.createChooser(shareIntent, shareLabel)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
break;
}
return Service.START_STICKY;
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/media/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
index 3c6e19f..7bc15dd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
@@ -72,7 +72,8 @@
@Mock private lateinit var lmmFactory: LocalMediaManagerFactory
@Mock private lateinit var lmm: LocalMediaManager
@Mock private lateinit var mr2: MediaRouter2Manager
- private lateinit var fakeExecutor: FakeExecutor
+ private lateinit var fakeFgExecutor: FakeExecutor
+ private lateinit var fakeBgExecutor: FakeExecutor
@Mock private lateinit var dumpster: DumpManager
@Mock private lateinit var listener: MediaDeviceManager.Listener
@Mock private lateinit var device: MediaDevice
@@ -87,9 +88,10 @@
@Before
fun setUp() {
- fakeExecutor = FakeExecutor(FakeSystemClock())
- manager = MediaDeviceManager(context, lmmFactory, mr2, fakeExecutor, mediaDataManager,
- dumpster)
+ fakeFgExecutor = FakeExecutor(FakeSystemClock())
+ fakeBgExecutor = FakeExecutor(FakeSystemClock())
+ manager = MediaDeviceManager(context, lmmFactory, mr2, fakeFgExecutor, fakeBgExecutor,
+ mediaDataManager, dumpster)
manager.addListener(listener)
// Configure mocks.
@@ -144,13 +146,15 @@
fun loadAndRemoveMediaData() {
manager.onMediaDataLoaded(KEY, null, mediaData)
manager.onMediaDataRemoved(KEY)
+ fakeBgExecutor.runAllReady()
verify(lmm).unregisterCallback(any())
}
@Test
fun loadMediaDataWithNullToken() {
manager.onMediaDataLoaded(KEY, null, mediaData.copy(token = null))
- fakeExecutor.runAllReady()
+ fakeBgExecutor.runAllReady()
+ fakeFgExecutor.runAllReady()
val data = captureDeviceData(KEY)
assertThat(data.enabled).isTrue()
assertThat(data.name).isEqualTo(DEVICE_NAME)
@@ -163,6 +167,8 @@
reset(listener)
// WHEN data is loaded with a new key
manager.onMediaDataLoaded(KEY, KEY_OLD, mediaData)
+ fakeBgExecutor.runAllReady()
+ fakeFgExecutor.runAllReady()
// THEN the listener for the old key should removed.
verify(lmm).unregisterCallback(any())
// AND a new device event emitted
@@ -186,6 +192,8 @@
fun unknownOldKey() {
val oldKey = "unknown"
manager.onMediaDataLoaded(KEY, oldKey, mediaData)
+ fakeBgExecutor.runAllReady()
+ fakeFgExecutor.runAllReady()
verify(listener).onMediaDeviceChanged(eq(KEY), eq(oldKey), any())
}
@@ -193,13 +201,16 @@
fun updateToSessionTokenWithNullRoute() {
// GIVEN that media data has been loaded with a null token
manager.onMediaDataLoaded(KEY, null, mediaData.copy(token = null))
+ fakeBgExecutor.runAllReady()
+ fakeFgExecutor.runAllReady()
+ reset(listener)
// WHEN media data is loaded with a different token
// AND that token results in a null route
- reset(listener)
whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
manager.onMediaDataLoaded(KEY, null, mediaData)
+ fakeBgExecutor.runAllReady()
+ fakeFgExecutor.runAllReady()
// THEN the device should be disabled
- fakeExecutor.runAllReady()
val data = captureDeviceData(KEY)
assertThat(data.enabled).isFalse()
assertThat(data.name).isNull()
@@ -210,7 +221,8 @@
fun deviceEventOnAddNotification() {
// WHEN a notification is added
manager.onMediaDataLoaded(KEY, null, mediaData)
- val deviceCallback = captureCallback()
+ fakeBgExecutor.runAllReady()
+ fakeFgExecutor.runAllReady()
// THEN the update is dispatched to the listener
val data = captureDeviceData(KEY)
assertThat(data.enabled).isTrue()
@@ -230,10 +242,12 @@
@Test
fun deviceListUpdate() {
manager.onMediaDataLoaded(KEY, null, mediaData)
+ fakeBgExecutor.runAllReady()
val deviceCallback = captureCallback()
// WHEN the device list changes
deviceCallback.onDeviceListUpdate(mutableListOf(device))
- assertThat(fakeExecutor.runAllReady()).isEqualTo(1)
+ assertThat(fakeBgExecutor.runAllReady()).isEqualTo(1)
+ assertThat(fakeFgExecutor.runAllReady()).isEqualTo(1)
// THEN the update is dispatched to the listener
val data = captureDeviceData(KEY)
assertThat(data.enabled).isTrue()
@@ -244,10 +258,12 @@
@Test
fun selectedDeviceStateChanged() {
manager.onMediaDataLoaded(KEY, null, mediaData)
+ fakeBgExecutor.runAllReady()
val deviceCallback = captureCallback()
// WHEN the selected device changes state
deviceCallback.onSelectedDeviceStateChanged(device, 1)
- assertThat(fakeExecutor.runAllReady()).isEqualTo(1)
+ assertThat(fakeBgExecutor.runAllReady()).isEqualTo(1)
+ assertThat(fakeFgExecutor.runAllReady()).isEqualTo(1)
// THEN the update is dispatched to the listener
val data = captureDeviceData(KEY)
assertThat(data.enabled).isTrue()
@@ -270,6 +286,8 @@
whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
// WHEN a notification is added
manager.onMediaDataLoaded(KEY, null, mediaData)
+ fakeBgExecutor.runAllReady()
+ fakeFgExecutor.runAllReady()
// THEN the device is disabled
val data = captureDeviceData(KEY)
assertThat(data.enabled).isFalse()
@@ -281,13 +299,16 @@
fun deviceDisabledWhenMR2ReturnsNullRouteInfoOnDeviceChanged() {
// GIVEN a notif is added
manager.onMediaDataLoaded(KEY, null, mediaData)
+ fakeBgExecutor.runAllReady()
+ fakeFgExecutor.runAllReady()
reset(listener)
// AND MR2Manager returns null for routing session
whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
// WHEN the selected device changes state
val deviceCallback = captureCallback()
deviceCallback.onSelectedDeviceStateChanged(device, 1)
- fakeExecutor.runAllReady()
+ fakeBgExecutor.runAllReady()
+ fakeFgExecutor.runAllReady()
// THEN the device is disabled
val data = captureDeviceData(KEY)
assertThat(data.enabled).isFalse()
@@ -299,13 +320,16 @@
fun deviceDisabledWhenMR2ReturnsNullRouteInfoOnDeviceListUpdate() {
// GIVEN a notif is added
manager.onMediaDataLoaded(KEY, null, mediaData)
+ fakeBgExecutor.runAllReady()
+ fakeFgExecutor.runAllReady()
reset(listener)
// GIVEN that MR2Manager returns null for routing session
whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
// WHEN the selected device changes state
val deviceCallback = captureCallback()
deviceCallback.onDeviceListUpdate(mutableListOf(device))
- fakeExecutor.runAllReady()
+ fakeBgExecutor.runAllReady()
+ fakeFgExecutor.runAllReady()
// THEN the device is disabled
val data = captureDeviceData(KEY)
assertThat(data.enabled).isFalse()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
index e98b6b6..4c9e141 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
@@ -32,7 +32,9 @@
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.settings.CurrentUserContextTracker;
+import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
import org.junit.Before;
import org.junit.Test;
@@ -61,6 +63,12 @@
private Executor mExecutor;
@Mock
private CurrentUserContextTracker mUserContextTracker;
+ private KeyguardDismissUtil mKeyguardDismissUtil = new KeyguardDismissUtil() {
+ public void executeWhenUnlocked(ActivityStarter.OnDismissAction action,
+ boolean requiresShadeOpen) {
+ action.onDismiss();
+ }
+ };
private RecordingService mRecordingService;
@@ -68,7 +76,7 @@
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mRecordingService = Mockito.spy(new RecordingService(mController, mExecutor, mUiEventLogger,
- mNotificationManager, mUserContextTracker));
+ mNotificationManager, mUserContextTracker, mKeyguardDismissUtil));
// Return actual context info
doReturn(mContext).when(mRecordingService).getApplicationContext();
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);