diff options
27 files changed, 1108 insertions, 32 deletions
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 233e09d90f9b..13a6be557dae 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -360,6 +360,23 @@ public class Notification implements Parcelable @Deprecated public RemoteViews headsUpContentView; + private boolean mUsesStandardHeader; + + private static final ArraySet<Integer> STANDARD_LAYOUTS = new ArraySet<>(); + static { + STANDARD_LAYOUTS.add(R.layout.notification_template_material_base); + STANDARD_LAYOUTS.add(R.layout.notification_template_material_big_base); + STANDARD_LAYOUTS.add(R.layout.notification_template_material_big_picture); + STANDARD_LAYOUTS.add(R.layout.notification_template_material_big_text); + STANDARD_LAYOUTS.add(R.layout.notification_template_material_inbox); + STANDARD_LAYOUTS.add(R.layout.notification_template_material_messaging); + STANDARD_LAYOUTS.add(R.layout.notification_template_material_media); + STANDARD_LAYOUTS.add(R.layout.notification_template_material_big_media); + STANDARD_LAYOUTS.add(R.layout.notification_template_ambient_header); + STANDARD_LAYOUTS.add(R.layout.notification_template_header); + STANDARD_LAYOUTS.add(R.layout.notification_template_material_ambient); + } + /** * A large bitmap to be shown in the notification content area. * @@ -2534,6 +2551,8 @@ public class Notification implements Parcelable } parcel.writeInt(mGroupAlertBehavior); + + // mUsesStandardHeader is not written because it should be recomputed in listeners } /** @@ -4092,6 +4111,25 @@ public class Notification implements Parcelable } } + /** + * @hide + */ + public boolean usesStandardHeader() { + if (mN.mUsesStandardHeader) { + return true; + } + if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) { + if (mN.contentView == null && mN.bigContentView == null) { + return true; + } + } + boolean contentViewUsesHeader = mN.contentView == null + || STANDARD_LAYOUTS.contains(mN.contentView.getLayoutId()); + boolean bigContentViewUsesHeader = mN.bigContentView == null + || STANDARD_LAYOUTS.contains(mN.bigContentView.getLayoutId()); + return contentViewUsesHeader && bigContentViewUsesHeader; + } + private void resetStandardTemplate(RemoteViews contentView) { resetNotificationHeader(contentView); resetContentMargins(contentView); @@ -4123,6 +4161,7 @@ public class Notification implements Parcelable contentView.setViewVisibility(R.id.time, View.GONE); contentView.setImageViewIcon(R.id.profile_badge, null); contentView.setViewVisibility(R.id.profile_badge, View.GONE); + mN.mUsesStandardHeader = false; } private void resetContentMargins(RemoteViews contentView) { @@ -4444,6 +4483,7 @@ public class Notification implements Parcelable bindProfileBadge(contentView); } bindExpandButton(contentView); + mN.mUsesStandardHeader = true; } private void bindExpandButton(RemoteViews contentView) { diff --git a/core/java/android/view/NotificationHeaderView.java b/core/java/android/view/NotificationHeaderView.java index fbba8abff304..137e820805e2 100644 --- a/core/java/android/view/NotificationHeaderView.java +++ b/core/java/android/view/NotificationHeaderView.java @@ -17,6 +17,7 @@ package android.view; import android.annotation.Nullable; +import android.app.AppOpsManager; import android.app.Notification; import android.content.Context; import android.content.res.Resources; @@ -25,6 +26,7 @@ import android.graphics.Canvas; import android.graphics.Outline; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.util.ArraySet; import android.util.AttributeSet; import android.widget.ImageView; import android.widget.RemoteViews; @@ -53,6 +55,10 @@ public class NotificationHeaderView extends ViewGroup { private ImageView mExpandButton; private CachingIconView mIcon; private View mProfileBadge; + private View mOverlayIcon; + private View mCameraIcon; + private View mMicIcon; + private View mAppOps; private int mIconColor; private int mOriginalNotificationColor; private boolean mExpanded; @@ -108,6 +114,10 @@ public class NotificationHeaderView extends ViewGroup { mExpandButton = findViewById(com.android.internal.R.id.expand_button); mIcon = findViewById(com.android.internal.R.id.icon); mProfileBadge = findViewById(com.android.internal.R.id.profile_badge); + mCameraIcon = findViewById(com.android.internal.R.id.camera); + mMicIcon = findViewById(com.android.internal.R.id.mic); + mOverlayIcon = findViewById(com.android.internal.R.id.overlay); + mAppOps = findViewById(com.android.internal.R.id.app_ops); } @Override @@ -198,6 +208,11 @@ public class NotificationHeaderView extends ViewGroup { layoutRight = end - paddingEnd; end = layoutLeft = layoutRight - child.getMeasuredWidth(); } + if (child == mAppOps) { + int paddingEnd = mContentEndMargin; + layoutRight = end - paddingEnd; + end = layoutLeft = layoutRight - child.getMeasuredWidth(); + } if (getLayoutDirection() == LAYOUT_DIRECTION_RTL) { int ltrLeft = layoutLeft; layoutLeft = getWidth() - layoutRight; @@ -289,6 +304,22 @@ public class NotificationHeaderView extends ViewGroup { updateExpandButton(); } + /** + * Shows or hides 'app op in use' icons based on app usage. + */ + public void showAppOpsIcons(ArraySet<Integer> appOps) { + if (mOverlayIcon == null || mCameraIcon == null || mMicIcon == null) { + return; + } + + mOverlayIcon.setVisibility(appOps.contains(AppOpsManager.OP_SYSTEM_ALERT_WINDOW) + ? View.VISIBLE : View.GONE); + mCameraIcon.setVisibility(appOps.contains(AppOpsManager.OP_CAMERA) + ? View.VISIBLE : View.GONE); + mMicIcon.setVisibility(appOps.contains(AppOpsManager.OP_RECORD_AUDIO) + ? View.VISIBLE : View.GONE); + } + private void updateExpandButton() { int drawableId; int contentDescriptionId; diff --git a/core/res/res/drawable/ic_alert_window_layer.xml b/core/res/res/drawable/ic_alert_window_layer.xml new file mode 100644 index 000000000000..15931b807918 --- /dev/null +++ b/core/res/res/drawable/ic_alert_window_layer.xml @@ -0,0 +1,24 @@ +<!-- +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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="16dp" + android:height="16dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M11.99,18.54l-7.37,-5.73L3,14.07l9,7 9,-7 -1.63,-1.27 -7.38,5.74zM12,16l7.36,-5.73L21,9l-9,-7 -9,7 1.63,1.27L12,16z"/> +</vector>
\ No newline at end of file diff --git a/core/res/res/drawable/ic_camera.xml b/core/res/res/drawable/ic_camera.xml new file mode 100644 index 000000000000..2921a689ef8a --- /dev/null +++ b/core/res/res/drawable/ic_camera.xml @@ -0,0 +1,27 @@ +<!-- +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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="16dp" + android:height="16dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:pathData="M12,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0" + android:fillColor="#FFFFFF"/> + <path + android:pathData="M9,2L7.17,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2L9,2zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z" + android:fillColor="#FFFFFF"/> +</vector>
\ No newline at end of file diff --git a/core/res/res/drawable/ic_mic.xml b/core/res/res/drawable/ic_mic.xml new file mode 100644 index 000000000000..3212330278aa --- /dev/null +++ b/core/res/res/drawable/ic_mic.xml @@ -0,0 +1,24 @@ +<!-- +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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="16dp" + android:height="16dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:pathData="M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11L5,11c0,3.41 2.72,6.23 6,6.72L11,21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z" + android:fillColor="#FFFFFF"/> +</vector>
\ No newline at end of file diff --git a/core/res/res/layout/notification_template_header.xml b/core/res/res/layout/notification_template_header.xml index 20bdf3fe8fa3..c03cf51d6bca 100644 --- a/core/res/res/layout/notification_template_header.xml +++ b/core/res/res/layout/notification_template_header.xml @@ -14,7 +14,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License --> - +<!-- extends ViewGroup --> <NotificationHeaderView xmlns:android="http://schemas.android.com/apk/res/android" android:theme="@style/Theme.Material.Notification" @@ -126,5 +126,42 @@ android:visibility="gone" android:contentDescription="@string/notification_work_profile_content_description" /> + + <LinearLayout + android:id="@+id/app_ops" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:orientation="horizontal" > + <ImageButton + android:id="@+id/camera" + android:layout_width="?attr/notificationHeaderIconSize" + android:layout_height="?attr/notificationHeaderIconSize" + android:src="@drawable/ic_camera" + android:tint="@color/notification_secondary_text_color_light" + android:background="?android:selectableItemBackgroundBorderless" + android:layout_marginStart="6dp" + android:visibility="gone" + /> + <ImageButton + android:id="@+id/mic" + android:layout_width="?attr/notificationHeaderIconSize" + android:layout_height="?attr/notificationHeaderIconSize" + android:src="@drawable/ic_mic" + android:tint="@color/notification_secondary_text_color_light" + android:background="?android:selectableItemBackgroundBorderless" + android:layout_marginStart="4dp" + android:visibility="gone" + /> + <ImageButton + android:id="@+id/overlay" + android:layout_width="?attr/notificationHeaderIconSize" + android:layout_height="?attr/notificationHeaderIconSize" + android:src="@drawable/ic_alert_window_layer" + android:tint="@color/notification_secondary_text_color_light" + android:background="?android:selectableItemBackgroundBorderless" + android:layout_marginStart="4dp" + android:visibility="gone" + /> + </LinearLayout> </NotificationHeaderView> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 1babd707c781..47abd04fcf6b 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -218,6 +218,10 @@ <java-symbol type="id" name="selection_end_handle" /> <java-symbol type="id" name="insertion_handle" /> <java-symbol type="id" name="accessibilityActionClickOnClickableSpan" /> + <java-symbol type="id" name="camera" /> + <java-symbol type="id" name="mic" /> + <java-symbol type="id" name="overlay" /> + <java-symbol type="id" name="app_ops" /> <java-symbol type="attr" name="actionModeShareDrawable" /> <java-symbol type="attr" name="alertDialogCenterButtons" /> @@ -1389,6 +1393,9 @@ <java-symbol type="drawable" name="stat_notify_mmcc_indication_icn" /> <java-symbol type="drawable" name="autofilled_highlight"/> + <java-symbol type="drawable" name="ic_camera" /> + <java-symbol type="drawable" name="ic_mic" /> + <java-symbol type="drawable" name="ic_alert_window_layer" /> <java-symbol type="drawable" name="ic_account_circle" /> <java-symbol type="color" name="user_icon_1" /> diff --git a/packages/SystemUI/res/layout/status_bar_notification_row.xml b/packages/SystemUI/res/layout/status_bar_notification_row.xml index 4614999e3c4f..2e7ab7fe8904 100644 --- a/packages/SystemUI/res/layout/status_bar_notification_row.xml +++ b/packages/SystemUI/res/layout/status_bar_notification_row.xml @@ -15,6 +15,7 @@ limitations under the License. --> +<!-- extends FrameLayout --> <com.android.systemui.statusbar.ExpandableNotificationRow xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" @@ -54,6 +55,7 @@ android:paddingStart="8dp" /> + <!-- TODO: remove --> <ImageButton android:id="@+id/helper" android:layout_width="48dp" @@ -64,7 +66,7 @@ android:tint="#FF0000" android:background="@drawable/ripple_drawable" android:visibility="visible" - /> + /> <ViewStub android:layout="@layout/notification_children_container" diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index 7403ddc441f6..cad155c43867 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -44,6 +44,7 @@ import com.android.systemui.power.EnhancedEstimates; import com.android.systemui.power.EnhancedEstimatesImpl; import com.android.systemui.power.PowerNotificationWarnings; import com.android.systemui.power.PowerUI; +import com.android.systemui.statusbar.AppOpsListener; import com.android.systemui.statusbar.phone.ConfigurationControllerImpl; import com.android.systemui.statusbar.phone.DarkIconDispatcherImpl; import com.android.systemui.statusbar.phone.LightBarController; @@ -314,6 +315,8 @@ public class Dependency extends SystemUI { mProviders.put(EnhancedEstimates.class, () -> new EnhancedEstimatesImpl()); + mProviders.put(AppOpsListener.class, () -> new AppOpsListener(mContext)); + // Put all dependencies above here so the factory can override them if it wants. SystemUIFactory.getInstance().injectDependencies(mProviders, mContext); } diff --git a/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java b/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java index a2c9ab4871c2..5a2263cf26c7 100644 --- a/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java +++ b/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java @@ -14,7 +14,9 @@ package com.android.systemui; +import android.annotation.Nullable; import android.service.notification.StatusBarNotification; +import android.util.ArraySet; public interface ForegroundServiceController { /** @@ -46,4 +48,32 @@ public interface ForegroundServiceController { * @return true if sbn is the system-provided "dungeon" (list of running foreground services). */ boolean isDungeonNotification(StatusBarNotification sbn); + + /** + * @return true if sbn is one of the window manager "drawing over other apps" notifications + */ + boolean isSystemAlertNotification(StatusBarNotification sbn); + + /** + * Returns the key of the foreground service from this package using the standard template, + * if one exists. + */ + @Nullable String getStandardLayoutKey(int userId, String pkg); + + /** + * @return true if this user/pkg has a missing or custom layout notification and therefore needs + * a disclosure notification for system alert windows. + */ + boolean isSystemAlertWarningNeeded(int userId, String pkg); + + /** + * Records active app ops. App Ops are stored in FSC in addition to NotificationData in + * case they change before we have a notification to tag. + */ + void onAppOpChanged(int code, int uid, String packageName, boolean active); + + /** + * Gets active app ops for this user and package. + */ + @Nullable ArraySet<Integer> getAppOps(int userId, String packageName); } diff --git a/packages/SystemUI/src/com/android/systemui/ForegroundServiceControllerImpl.java b/packages/SystemUI/src/com/android/systemui/ForegroundServiceControllerImpl.java index 3714c4ea7e2c..fc2b5b490e2c 100644 --- a/packages/SystemUI/src/com/android/systemui/ForegroundServiceControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/ForegroundServiceControllerImpl.java @@ -18,13 +18,13 @@ import android.app.Notification; import android.app.NotificationManager; import android.content.Context; import android.os.Bundle; +import android.os.UserHandle; import android.service.notification.StatusBarNotification; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import android.util.SparseArray; -import com.android.internal.annotations.VisibleForTesting; import com.android.internal.messages.nano.SystemMessageProto; import java.util.Arrays; @@ -34,17 +34,19 @@ import java.util.Arrays; */ public class ForegroundServiceControllerImpl implements ForegroundServiceController { - + // shelf life of foreground services before they go bad public static final long FG_SERVICE_GRACE_MILLIS = 5000; private static final String TAG = "FgServiceController"; private static final boolean DBG = false; + private final Context mContext; private final SparseArray<UserServices> mUserServices = new SparseArray<>(); private final Object mMutex = new Object(); public ForegroundServiceControllerImpl(Context context) { + mContext = context; } @Override @@ -57,6 +59,52 @@ public class ForegroundServiceControllerImpl } @Override + public boolean isSystemAlertWarningNeeded(int userId, String pkg) { + synchronized (mMutex) { + final UserServices services = mUserServices.get(userId); + if (services == null) return false; + return services.getStandardLayoutKey(pkg) == null; + } + } + + @Override + public String getStandardLayoutKey(int userId, String pkg) { + synchronized (mMutex) { + final UserServices services = mUserServices.get(userId); + if (services == null) return null; + return services.getStandardLayoutKey(pkg); + } + } + + @Override + public ArraySet<Integer> getAppOps(int userId, String pkg) { + synchronized (mMutex) { + final UserServices services = mUserServices.get(userId); + if (services == null) { + return null; + } + return services.getFeatures(pkg); + } + } + + @Override + public void onAppOpChanged(int code, int uid, String packageName, boolean active) { + int userId = UserHandle.getUserId(uid); + synchronized (mMutex) { + UserServices userServices = mUserServices.get(userId); + if (userServices == null) { + userServices = new UserServices(); + mUserServices.put(userId, userServices); + } + if (active) { + userServices.addOp(packageName, code); + } else { + userServices.removeOp(packageName, code); + } + } + } + + @Override public void addNotification(StatusBarNotification sbn, int importance) { updateNotification(sbn, importance); } @@ -102,9 +150,16 @@ public class ForegroundServiceControllerImpl } } else { userServices.removeNotification(sbn.getPackageName(), sbn.getKey()); - if (0 != (sbn.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE) - && newImportance > NotificationManager.IMPORTANCE_MIN) { - userServices.addNotification(sbn.getPackageName(), sbn.getKey()); + if (0 != (sbn.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE)) { + if (newImportance > NotificationManager.IMPORTANCE_MIN) { + userServices.addImportantNotification(sbn.getPackageName(), sbn.getKey()); + } + final Notification.Builder builder = Notification.Builder.recoverBuilder( + mContext, sbn.getNotification()); + if (builder.usesStandardHeader()) { + userServices.addStandardLayoutNotification( + sbn.getPackageName(), sbn.getKey()); + } } } } @@ -117,42 +172,105 @@ public class ForegroundServiceControllerImpl && sbn.getPackageName().equals("android"); } + @Override + public boolean isSystemAlertNotification(StatusBarNotification sbn) { + // TODO: tag system alert notifications so they can be suppressed if app's notification + // is tagged + return false; + } + /** * Struct to track relevant packages and notifications for a userid's foreground services. */ private static class UserServices { private String[] mRunning = null; private long mServiceStartTime = 0; - private ArrayMap<String, ArraySet<String>> mNotifications = new ArrayMap<>(1); + // package -> sufficiently important posted notification keys + private ArrayMap<String, ArraySet<String>> mImportantNotifications = new ArrayMap<>(1); + // package -> standard layout posted notification keys + private ArrayMap<String, ArraySet<String>> mStandardLayoutNotifications = new ArrayMap<>(1); + + // package -> app ops + private ArrayMap<String, ArraySet<Integer>> mAppOps = new ArrayMap<>(1); + public void setRunningServices(String[] pkgs, long serviceStartTime) { mRunning = pkgs != null ? Arrays.copyOf(pkgs, pkgs.length) : null; mServiceStartTime = serviceStartTime; } - public void addNotification(String pkg, String key) { - if (mNotifications.get(pkg) == null) { - mNotifications.put(pkg, new ArraySet<String>()); + + public void addOp(String pkg, int op) { + if (mAppOps.get(pkg) == null) { + mAppOps.put(pkg, new ArraySet<>(3)); + } + mAppOps.get(pkg).add(op); + } + + public boolean removeOp(String pkg, int op) { + final boolean found; + final ArraySet<Integer> keys = mAppOps.get(pkg); + if (keys == null) { + found = false; + } else { + found = keys.remove(op); + if (keys.size() == 0) { + mAppOps.remove(pkg); + } } - mNotifications.get(pkg).add(key); + return found; } + + public void addImportantNotification(String pkg, String key) { + addNotification(mImportantNotifications, pkg, key); + } + + public boolean removeImportantNotification(String pkg, String key) { + return removeNotification(mImportantNotifications, pkg, key); + } + + public void addStandardLayoutNotification(String pkg, String key) { + addNotification(mStandardLayoutNotifications, pkg, key); + } + + public boolean removeStandardLayoutNotification(String pkg, String key) { + return removeNotification(mStandardLayoutNotifications, pkg, key); + } + public boolean removeNotification(String pkg, String key) { + boolean removed = false; + removed |= removeImportantNotification(pkg, key); + removed |= removeStandardLayoutNotification(pkg, key); + return removed; + } + + public void addNotification(ArrayMap<String, ArraySet<String>> map, String pkg, + String key) { + if (map.get(pkg) == null) { + map.put(pkg, new ArraySet<>()); + } + map.get(pkg).add(key); + } + + public boolean removeNotification(ArrayMap<String, ArraySet<String>> map, + String pkg, String key) { final boolean found; - final ArraySet<String> keys = mNotifications.get(pkg); + final ArraySet<String> keys = map.get(pkg); if (keys == null) { found = false; } else { found = keys.remove(key); if (keys.size() == 0) { - mNotifications.remove(pkg); + map.remove(pkg); } } return found; } + public boolean isDungeonNeeded() { if (mRunning != null && System.currentTimeMillis() - mServiceStartTime >= FG_SERVICE_GRACE_MILLIS) { for (String pkg : mRunning) { - final ArraySet<String> set = mNotifications.get(pkg); + final ArraySet<String> set = mImportantNotifications.get(pkg); if (set == null || set.size() == 0) { return true; } @@ -160,5 +278,27 @@ public class ForegroundServiceControllerImpl } return false; } + + public ArraySet<Integer> getFeatures(String pkg) { + return mAppOps.get(pkg); + } + + public String getStandardLayoutKey(String pkg) { + final ArraySet<String> set = mStandardLayoutNotifications.get(pkg); + if (set == null || set.size() == 0) { + return null; + } + return set.valueAt(0); + } + + @Override + public String toString() { + return "UserServices{" + + "mRunning=" + Arrays.toString(mRunning) + + ", mServiceStartTime=" + mServiceStartTime + + ", mImportantNotifications=" + mImportantNotifications + + ", mStandardLayoutNotifications=" + mStandardLayoutNotifications + + '}'; + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AppOpsListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/AppOpsListener.java new file mode 100644 index 000000000000..2ec78cfe9382 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/AppOpsListener.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2017 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; + +import android.app.AppOpsManager; +import android.content.Context; + +import com.android.systemui.Dependency; +import com.android.systemui.ForegroundServiceController; + +/** + * This class handles listening to notification updates and passing them along to + * NotificationPresenter to be displayed to the user. + */ +public class AppOpsListener implements AppOpsManager.OnOpActiveChangedListener { + private static final String TAG = "NotificationListener"; + + // Dependencies: + private final ForegroundServiceController mFsc = + Dependency.get(ForegroundServiceController.class); + + private final Context mContext; + protected NotificationPresenter mPresenter; + protected NotificationEntryManager mEntryManager; + protected final AppOpsManager mAppOps; + + protected static final int[] OPS = new int[] {AppOpsManager.OP_CAMERA, + AppOpsManager.OP_SYSTEM_ALERT_WINDOW, + AppOpsManager.OP_RECORD_AUDIO}; + + public AppOpsListener(Context context) { + mContext = context; + mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); + } + + public void setUpWithPresenter(NotificationPresenter presenter, + NotificationEntryManager entryManager) { + mPresenter = presenter; + mEntryManager = entryManager; + mAppOps.startWatchingActive(OPS, this); + } + + public void destroy() { + mAppOps.stopWatchingActive(this); + } + + @Override + public void onOpActiveChanged(int code, int uid, String packageName, boolean active) { + mFsc.onAppOpChanged(code, uid, packageName, active); + mPresenter.getHandler().post(() -> { + mEntryManager.updateNotificationsForAppOps(code, uid, packageName, active); + }); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java index bc2dff917b9a..785fc1cc5922 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java @@ -36,6 +36,7 @@ import android.os.Build; import android.os.Bundle; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; +import android.util.ArraySet; import android.util.AttributeSet; import android.util.FloatProperty; import android.util.MathUtils; @@ -1354,6 +1355,14 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mHelperButton.setVisibility(show ? View.VISIBLE : View.GONE); } + public void showAppOpsIcons(ArraySet<Integer> activeOps) { + if (mIsSummaryWithChildren && mChildrenContainer.getHeaderView() != null) { + mChildrenContainer.getHeaderView().showAppOpsIcons(activeOps); + } + mPrivateLayout.showAppOpsIcons(activeOps); + mPublicLayout.showAppOpsIcons(activeOps); + } + @Override protected void onFinishInflate() { super.onFinishInflate(); @@ -2629,6 +2638,16 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mChildrenContainer = childrenContainer; } + @VisibleForTesting + protected void setPrivateLayout(NotificationContentView privateLayout) { + mPrivateLayout = privateLayout; + } + + @VisibleForTesting + protected void setPublicLayout(NotificationContentView publicLayout) { + mPublicLayout = publicLayout; + } + /** * Equivalent to View.OnLongClickListener with coordinates */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java index 91960df9b01d..73c87953cf45 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java @@ -23,6 +23,7 @@ import android.content.Context; import android.graphics.Rect; import android.os.Build; import android.service.notification.StatusBarNotification; +import android.util.ArraySet; import android.util.AttributeSet; import android.util.Log; import android.view.NotificationHeaderView; @@ -1423,6 +1424,17 @@ public class NotificationContentView extends FrameLayout { return header; } + public void showAppOpsIcons(ArraySet<Integer> activeOps) { + if (mContractedChild != null && mContractedWrapper.getNotificationHeader() != null) { + mContractedWrapper.getNotificationHeader().showAppOpsIcons(activeOps); + } + if (mExpandedChild != null && mExpandedWrapper.getNotificationHeader() != null) { + mExpandedWrapper.getNotificationHeader().showAppOpsIcons(activeOps); + } + if (mHeadsUpChild != null && mHeadsUpWrapper.getNotificationHeader() != null) { + mHeadsUpWrapper.getNotificationHeader().showAppOpsIcons(activeOps); + } + } public NotificationHeaderView getContractedNotificationHeader() { if (mContractedChild != null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java index 127f3f918fba..d53cb03cfcb7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java @@ -17,6 +17,7 @@ package com.android.systemui.statusbar; import android.app.AppGlobals; +import android.app.AppOpsManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; @@ -34,6 +35,7 @@ import android.service.notification.NotificationListenerService.RankingMap; import android.service.notification.SnoozeCriterion; import android.service.notification.StatusBarNotification; import android.util.ArrayMap; +import android.util.ArraySet; import android.view.View; import android.widget.ImageView; import android.widget.RemoteViews; @@ -65,6 +67,8 @@ public class NotificationData { private final Environment mEnvironment; private HeadsUpManager mHeadsUpManager; + final ForegroundServiceController mFsc = Dependency.get(ForegroundServiceController.class); + public static final class Entry { private static final long LAUNCH_COOLDOWN = 2000; private static final long REMOTE_INPUT_COOLDOWN = 500; @@ -95,6 +99,7 @@ public class NotificationData { private Throwable mDebugThrowable; public CharSequence remoteInputTextWhenReset; public long lastRemoteInputSent = NOT_LAUNCHED_YET; + public ArraySet<Integer> mActiveAppOps = new ArraySet<>(3); public Entry(StatusBarNotification n) { this.key = n.getKey(); @@ -194,7 +199,7 @@ public class NotificationData { /** * Update the notification icons. * @param context the context to create the icons with. - * @param n the notification to read the icon from. + * @param sbn the notification to read the icon from. * @throws InflationException */ public void updateIcons(Context context, StatusBarNotification sbn) @@ -375,6 +380,8 @@ public class NotificationData { } mGroupManager.onEntryAdded(entry); + updateAppOps(entry); + updateRankingAndSort(mRankingMap); } @@ -393,6 +400,35 @@ public class NotificationData { updateRankingAndSort(ranking); } + private void updateAppOps(Entry entry) { + final int uid = entry.notification.getUid(); + final String pkg = entry.notification.getPackageName(); + ArraySet<Integer> activeOps = mFsc.getAppOps(entry.notification.getUserId(), pkg); + if (activeOps != null) { + int N = activeOps.size(); + for (int i = 0; i < N; i++) { + updateAppOp(activeOps.valueAt(i), uid, pkg, true); + } + } + } + + public void updateAppOp(int appOp, int uid, String pkg, boolean showIcon) { + synchronized (mEntries) { + final int N = mEntries.size(); + for (int i = 0; i < N; i++) { + Entry entry = mEntries.valueAt(i); + if (uid == entry.notification.getUid() + && pkg.equals(entry.notification.getPackageName())) { + if (showIcon) { + entry.mActiveAppOps.add(appOp); + } else { + entry.mActiveAppOps.remove(appOp); + } + } + } + } + } + public boolean isAmbient(String key) { if (mRankingMap != null) { getRanking(key, mTmpRanking); @@ -545,11 +581,14 @@ public class NotificationData { return true; } - final ForegroundServiceController fsc = Dependency.get(ForegroundServiceController.class); - if (fsc.isDungeonNotification(sbn) && !fsc.isDungeonNeededForUser(sbn.getUserId())) { + if (mFsc.isDungeonNotification(sbn) && !mFsc.isDungeonNeededForUser(sbn.getUserId())) { // this is a foreground-service disclosure for a user that does not need to show one return true; } + if (mFsc.isSystemAlertNotification(sbn) && !mFsc.isSystemAlertWarningNeeded( + sbn.getUserId(), sbn.getPackageName())) { + return true; + } return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationEntryManager.java index 7360486ac7e9..71f7911b41f1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationEntryManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationEntryManager.java @@ -31,6 +31,7 @@ import android.os.PowerManager; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; +import android.os.UserHandle; import android.provider.Settings; import android.service.notification.NotificationListenerService; import android.service.notification.NotificationStats; @@ -77,7 +78,7 @@ import java.util.List; public class NotificationEntryManager implements Dumpable, NotificationInflater.InflationCallback, ExpandableNotificationRow.ExpansionLogger, NotificationUpdateHandler, VisualStabilityManager.Callback { - private static final String TAG = "NotificationEntryManager"; + private static final String TAG = "NotificationEntryMgr"; protected static final boolean DEBUG = false; protected static final boolean ENABLE_HEADS_UP = true; protected static final String SETTING_HEADS_UP_TICKER = "ticker_gets_heads_up"; @@ -734,6 +735,14 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater. } } + public void updateNotificationsForAppOps(int appOp, int uid, String pkg, boolean showIcon) { + if (mForegroundServiceController.getStandardLayoutKey( + UserHandle.getUserId(uid), pkg) != null) { + mNotificationData.updateAppOp(appOp, uid, pkg, showIcon); + updateNotifications(); + } + } + private boolean alertAgain(NotificationData.Entry oldEntry, Notification newNotification) { return oldEntry == null || !oldEntry.hasInterrupted() || (newNotification.flags & Notification.FLAG_ONLY_ALERT_ONCE) == 0; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java index cd4c7ae8d57e..75b8b371119e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java @@ -342,6 +342,8 @@ public class NotificationViewHierarchyManager { row.showBlockingHelper(entry.userSentiment == NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE); + + row.showAppOpsIcons(entry.mActiveAppOps); } mPresenter.onUpdateRowStates(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java index 2afd05f18f7a..cccdb58a20f9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -180,6 +180,7 @@ import com.android.systemui.recents.misc.SystemServicesProxy; import com.android.systemui.stackdivider.Divider; import com.android.systemui.stackdivider.WindowManagerProxy; import com.android.systemui.statusbar.ActivatableNotificationView; +import com.android.systemui.statusbar.AppOpsListener; import com.android.systemui.statusbar.BackDropView; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.CrossFadeHelper; @@ -405,6 +406,7 @@ public class StatusBar extends SystemUI implements DemoMode, protected NotificationLogger mNotificationLogger; protected NotificationEntryManager mEntryManager; protected NotificationViewHierarchyManager mViewHierarchyManager; + protected AppOpsListener mAppOpsListener; /** * Helper that is responsible for showing the right toast when a disallowed activity operation @@ -622,6 +624,8 @@ public class StatusBar extends SystemUI implements DemoMode, mMediaManager = Dependency.get(NotificationMediaManager.class); mEntryManager = Dependency.get(NotificationEntryManager.class); mViewHierarchyManager = Dependency.get(NotificationViewHierarchyManager.class); + mAppOpsListener = Dependency.get(AppOpsListener.class); + mAppOpsListener.setUpWithPresenter(this, mEntryManager); mColorExtractor = Dependency.get(SysuiColorExtractor.class); mColorExtractor.addOnColorsChangedListener(this); @@ -3293,6 +3297,7 @@ public class StatusBar extends SystemUI implements DemoMode, Dependency.get(ActivityStarterDelegate.class).setActivityStarterImpl(null); mDeviceProvisionedController.removeCallback(mUserSetupObserver); Dependency.get(ConfigurationController.class).removeCallback(this); + mAppOpsListener.destroy(); } private boolean mDemoModeAllowed; diff --git a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java index 943020c7b28e..18dd3c734660 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java @@ -16,6 +16,14 @@ package com.android.systemui; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNull; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import android.annotation.UserIdInt; import android.app.Notification; import android.app.NotificationManager; @@ -24,17 +32,14 @@ import android.os.UserHandle; import android.service.notification.StatusBarNotification; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; +import android.widget.RemoteViews; + import com.android.internal.messages.nano.SystemMessageProto; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - @SmallTest @RunWith(AndroidJUnit4.class) public class ForegroundServiceControllerTest extends SysuiTestCase { @@ -49,7 +54,7 @@ public class ForegroundServiceControllerTest extends SysuiTestCase { } @Test - public void testNotificationCRUD() { + public void testNotificationCRUD_dungeon() { StatusBarNotification sbn_user1_app1_fg = makeMockFgSBN(USERID_ONE, "com.example.app1"); StatusBarNotification sbn_user2_app2_fg = makeMockFgSBN(USERID_TWO, "com.example.app2"); StatusBarNotification sbn_user1_app3_fg = makeMockFgSBN(USERID_ONE, "com.example.app3"); @@ -98,6 +103,101 @@ public class ForegroundServiceControllerTest extends SysuiTestCase { } @Test + public void testNotificationCRUD_stdLayout() { + StatusBarNotification sbn_user1_app1_fg = + makeMockFgSBN(USERID_ONE, "com.example.app1", 0, true); + StatusBarNotification sbn_user2_app2_fg = + makeMockFgSBN(USERID_TWO, "com.example.app2", 1, true); + StatusBarNotification sbn_user1_app3_fg = + makeMockFgSBN(USERID_ONE, "com.example.app3", 2, true); + StatusBarNotification sbn_user1_app1 = makeMockSBN(USERID_ONE, "com.example.app1", + 5000, "monkeys", Notification.FLAG_AUTO_CANCEL); + StatusBarNotification sbn_user2_app1 = makeMockSBN(USERID_TWO, "com.example.app1", + 5000, "monkeys", Notification.FLAG_AUTO_CANCEL); + + assertFalse(fsc.removeNotification(sbn_user1_app3_fg)); + assertFalse(fsc.removeNotification(sbn_user2_app2_fg)); + assertFalse(fsc.removeNotification(sbn_user1_app1_fg)); + assertFalse(fsc.removeNotification(sbn_user1_app1)); + assertFalse(fsc.removeNotification(sbn_user2_app1)); + + fsc.addNotification(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_MIN); + fsc.addNotification(sbn_user2_app2_fg, NotificationManager.IMPORTANCE_MIN); + fsc.addNotification(sbn_user1_app3_fg, NotificationManager.IMPORTANCE_MIN); + fsc.addNotification(sbn_user1_app1, NotificationManager.IMPORTANCE_MIN); + fsc.addNotification(sbn_user2_app1, NotificationManager.IMPORTANCE_MIN); + + // these are never added to the tracker + assertFalse(fsc.removeNotification(sbn_user1_app1)); + assertFalse(fsc.removeNotification(sbn_user2_app1)); + + fsc.updateNotification(sbn_user1_app1, NotificationManager.IMPORTANCE_MIN); + fsc.updateNotification(sbn_user2_app1, NotificationManager.IMPORTANCE_MIN); + // should still not be there + assertFalse(fsc.removeNotification(sbn_user1_app1)); + assertFalse(fsc.removeNotification(sbn_user2_app1)); + + fsc.updateNotification(sbn_user2_app2_fg, NotificationManager.IMPORTANCE_MIN); + fsc.updateNotification(sbn_user1_app3_fg, NotificationManager.IMPORTANCE_MIN); + fsc.updateNotification(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_MIN); + + assertTrue(fsc.removeNotification(sbn_user1_app3_fg)); + assertFalse(fsc.removeNotification(sbn_user1_app3_fg)); + + assertTrue(fsc.removeNotification(sbn_user2_app2_fg)); + assertFalse(fsc.removeNotification(sbn_user2_app2_fg)); + + assertTrue(fsc.removeNotification(sbn_user1_app1_fg)); + assertFalse(fsc.removeNotification(sbn_user1_app1_fg)); + + assertFalse(fsc.removeNotification(sbn_user1_app1)); + assertFalse(fsc.removeNotification(sbn_user2_app1)); + } + + @Test + public void testAppOpsCRUD() { + // no crash on remove that doesn't exist + fsc.onAppOpChanged(9, 1000, "pkg1", false); + assertNull(fsc.getAppOps(0, "pkg1")); + + // multiuser & multipackage + fsc.onAppOpChanged(8, 50, "pkg1", true); + fsc.onAppOpChanged(1, 60, "pkg3", true); + fsc.onAppOpChanged(7, 500000, "pkg2", true); + + assertEquals(1, fsc.getAppOps(0, "pkg1").size()); + assertTrue(fsc.getAppOps(0, "pkg1").contains(8)); + + assertEquals(1, fsc.getAppOps(UserHandle.getUserId(500000), "pkg2").size()); + assertTrue(fsc.getAppOps(UserHandle.getUserId(500000), "pkg2").contains(7)); + + assertEquals(1, fsc.getAppOps(0, "pkg3").size()); + assertTrue(fsc.getAppOps(0, "pkg3").contains(1)); + + // multiple ops for the same package + fsc.onAppOpChanged(9, 50, "pkg1", true); + fsc.onAppOpChanged(5, 50, "pkg1", true); + + assertEquals(3, fsc.getAppOps(0, "pkg1").size()); + assertTrue(fsc.getAppOps(0, "pkg1").contains(8)); + assertTrue(fsc.getAppOps(0, "pkg1").contains(9)); + assertTrue(fsc.getAppOps(0, "pkg1").contains(5)); + + assertEquals(1, fsc.getAppOps(UserHandle.getUserId(500000), "pkg2").size()); + assertTrue(fsc.getAppOps(UserHandle.getUserId(500000), "pkg2").contains(7)); + + // remove one of the multiples + fsc.onAppOpChanged(9, 50, "pkg1", false); + assertEquals(2, fsc.getAppOps(0, "pkg1").size()); + assertTrue(fsc.getAppOps(0, "pkg1").contains(8)); + assertTrue(fsc.getAppOps(0, "pkg1").contains(5)); + + // remove last op + fsc.onAppOpChanged(1, 60, "pkg3", false); + assertNull(fsc.getAppOps(0, "pkg3")); + } + + @Test public void testDungeonPredicate() { StatusBarNotification sbn_user1_app1 = makeMockSBN(USERID_ONE, "com.example.app1", 5000, "monkeys", Notification.FLAG_AUTO_CANCEL); @@ -252,6 +352,14 @@ public class ForegroundServiceControllerTest extends SysuiTestCase { assertFalse(fsc.isDungeonNeededForUser(USERID_TWO)); assertTrue(fsc.isDungeonNeededForUser(USERID_ONE)); + // importance upgrade + fsc.addNotification(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_MIN); + assertTrue(fsc.isDungeonNeededForUser(USERID_ONE)); + assertFalse(fsc.isDungeonNeededForUser(USERID_TWO)); + sbn_user1_app1.getNotification().flags |= Notification.FLAG_FOREGROUND_SERVICE; + fsc.updateNotification(sbn_user1_app1_fg, + NotificationManager.IMPORTANCE_DEFAULT); // this is now a fg notification + // finally, let's turn off the service fsc.addNotification(makeMockDungeon(USERID_ONE, null), NotificationManager.IMPORTANCE_DEFAULT); @@ -260,12 +368,71 @@ public class ForegroundServiceControllerTest extends SysuiTestCase { assertFalse(fsc.isDungeonNeededForUser(USERID_TWO)); } + @Test + public void testStdLayoutBasic() { + final String PKG1 = "com.example.app0"; + + StatusBarNotification sbn_user1_app1 = makeMockFgSBN(USERID_ONE, PKG1, 0, true); + sbn_user1_app1.getNotification().flags = 0; + StatusBarNotification sbn_user1_app1_fg = makeMockFgSBN(USERID_ONE, PKG1, 1, true); + fsc.addNotification(sbn_user1_app1, NotificationManager.IMPORTANCE_MIN); // not fg + assertTrue(fsc.isSystemAlertWarningNeeded(USERID_ONE, PKG1)); // should be required! + fsc.addNotification(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_MIN); + assertFalse(fsc.isSystemAlertWarningNeeded(USERID_ONE, PKG1)); // app1 has got it covered + assertFalse(fsc.isSystemAlertWarningNeeded(USERID_TWO, "otherpkg")); + // let's take out the non-fg notification and see what happens. + fsc.removeNotification(sbn_user1_app1); + // still covered by sbn_user1_app1_fg + assertFalse(fsc.isSystemAlertWarningNeeded(USERID_ONE, PKG1)); + assertFalse(fsc.isSystemAlertWarningNeeded(USERID_TWO, "anyPkg")); + + // let's attempt to downgrade the notification from FLAG_FOREGROUND and see what we get + StatusBarNotification sbn_user1_app1_fg_sneaky = makeMockFgSBN(USERID_ONE, PKG1, 1, true); + sbn_user1_app1_fg_sneaky.getNotification().flags = 0; + fsc.updateNotification(sbn_user1_app1_fg_sneaky, NotificationManager.IMPORTANCE_MIN); + assertTrue(fsc.isSystemAlertWarningNeeded(USERID_ONE, PKG1)); // should be required! + assertFalse(fsc.isSystemAlertWarningNeeded(USERID_TWO, "anything")); + // ok, ok, we'll put it back + sbn_user1_app1_fg_sneaky.getNotification().flags = Notification.FLAG_FOREGROUND_SERVICE; + fsc.updateNotification(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_MIN); + assertFalse(fsc.isSystemAlertWarningNeeded(USERID_ONE, PKG1)); + assertFalse(fsc.isSystemAlertWarningNeeded(USERID_TWO, "whatever")); + + assertTrue(fsc.removeNotification(sbn_user1_app1_fg_sneaky)); + assertTrue(fsc.isSystemAlertWarningNeeded(USERID_ONE, PKG1)); // should be required! + assertFalse(fsc.isSystemAlertWarningNeeded(USERID_TWO, "a")); + + // let's try a custom layout + sbn_user1_app1_fg_sneaky = makeMockFgSBN(USERID_ONE, PKG1, 1, false); + fsc.updateNotification(sbn_user1_app1_fg_sneaky, NotificationManager.IMPORTANCE_MIN); + assertTrue(fsc.isSystemAlertWarningNeeded(USERID_ONE, PKG1)); // should be required! + assertFalse(fsc.isSystemAlertWarningNeeded(USERID_TWO, "anything")); + // now let's test an upgrade (non fg to fg) + fsc.addNotification(sbn_user1_app1, NotificationManager.IMPORTANCE_MIN); + assertTrue(fsc.isSystemAlertWarningNeeded(USERID_ONE, PKG1)); + assertFalse(fsc.isSystemAlertWarningNeeded(USERID_TWO, "b")); + sbn_user1_app1.getNotification().flags |= Notification.FLAG_FOREGROUND_SERVICE; + fsc.updateNotification(sbn_user1_app1, + NotificationManager.IMPORTANCE_MIN); // this is now a fg notification + + assertFalse(fsc.isSystemAlertWarningNeeded(USERID_TWO, PKG1)); + assertFalse(fsc.isSystemAlertWarningNeeded(USERID_ONE, PKG1)); + + // remove it, make sure we're out of compliance again + assertTrue(fsc.removeNotification(sbn_user1_app1)); // was fg, should return true + assertFalse(fsc.removeNotification(sbn_user1_app1)); + assertFalse(fsc.isSystemAlertWarningNeeded(USERID_TWO, PKG1)); + assertTrue(fsc.isSystemAlertWarningNeeded(USERID_ONE, PKG1)); + } + private StatusBarNotification makeMockSBN(int userid, String pkg, int id, String tag, int flags) { final Notification n = mock(Notification.class); + n.extras = new Bundle(); n.flags = flags; return makeMockSBN(userid, pkg, id, tag, n); } + private StatusBarNotification makeMockSBN(int userid, String pkg, int id, String tag, Notification n) { final StatusBarNotification sbn = mock(StatusBarNotification.class); @@ -278,9 +445,25 @@ public class ForegroundServiceControllerTest extends SysuiTestCase { when(sbn.getKey()).thenReturn("MOCK:"+userid+"|"+pkg+"|"+id+"|"+tag); return sbn; } + + private StatusBarNotification makeMockFgSBN(int userid, String pkg, int id, + boolean usesStdLayout) { + StatusBarNotification sbn = + makeMockSBN(userid, pkg, id, "foo", Notification.FLAG_FOREGROUND_SERVICE); + if (usesStdLayout) { + sbn.getNotification().contentView = null; + sbn.getNotification().headsUpContentView = null; + sbn.getNotification().bigContentView = null; + } else { + sbn.getNotification().contentView = mock(RemoteViews.class); + } + return sbn; + } + private StatusBarNotification makeMockFgSBN(int userid, String pkg) { return makeMockSBN(userid, pkg, 1000, "foo", Notification.FLAG_FOREGROUND_SERVICE); } + private StatusBarNotification makeMockDungeon(int userid, String[] pkgs) { final Notification n = mock(Notification.class); n.flags = Notification.FLAG_ONGOING_EVENT; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/AppOpsListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/AppOpsListenerTest.java new file mode 100644 index 000000000000..2a48c4b67e0e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/AppOpsListenerTest.java @@ -0,0 +1,102 @@ +/* + * 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; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.AppOpsManager; +import android.os.Handler; +import android.os.Looper; +import android.support.test.filters.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import com.android.systemui.ForegroundServiceController; +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class AppOpsListenerTest extends SysuiTestCase { + private static final String TEST_PACKAGE_NAME = "test"; + private static final int TEST_UID = 0; + + @Mock private NotificationPresenter mPresenter; + @Mock private AppOpsManager mAppOpsManager; + + // Dependency mocks: + @Mock private NotificationEntryManager mEntryManager; + @Mock private ForegroundServiceController mFsc; + + private AppOpsListener mListener; + private Handler mHandler; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mDependency.injectTestDependency(NotificationEntryManager.class, mEntryManager); + mDependency.injectTestDependency(ForegroundServiceController.class, mFsc); + getContext().addMockSystemService(AppOpsManager.class, mAppOpsManager); + mHandler = new Handler(Looper.getMainLooper()); + when(mPresenter.getHandler()).thenReturn(mHandler); + + mListener = new AppOpsListener(mContext); + } + + @Test + public void testOnlyListenForFewOps() { + mListener.setUpWithPresenter(mPresenter, mEntryManager); + + verify(mAppOpsManager, times(1)).startWatchingActive(AppOpsListener.OPS, mListener); + } + + @Test + public void testStopListening() { + mListener.destroy(); + verify(mAppOpsManager, times(1)).stopWatchingActive(mListener); + } + + @Test + public void testInformEntryMgrOnAppOpsChange() { + mListener.setUpWithPresenter(mPresenter, mEntryManager); + mListener.onOpActiveChanged( + AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true); + waitForIdleSync(mHandler); + verify(mEntryManager, times(1)).updateNotificationsForAppOps( + AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true); + } + + @Test + public void testInformFscOnAppOpsChange() { + mListener.setUpWithPresenter(mPresenter, mEntryManager); + mListener.onOpActiveChanged( + AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true); + waitForIdleSync(mHandler); + verify(mFsc, times(1)).onAppOpChanged( + AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ExpandableNotificationRowTest.java index 544585a4a917..ce629bb41e7b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ExpandableNotificationRowTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ExpandableNotificationRowTest.java @@ -19,10 +19,15 @@ package com.android.systemui.statusbar; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import android.app.AppOpsManager; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; +import android.util.ArraySet; +import android.view.NotificationHeaderView; import android.view.View; import com.android.systemui.SysuiTestCase; @@ -146,4 +151,34 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { Assert.assertTrue("Should always play sounds when not trusted.", mGroup.isSoundEffectsEnabled()); } + + @Test + public void testShowAppOpsIcons_noHeader() { + // public notification is custom layout - no header + mGroup.setSensitive(true, true); + mGroup.showAppOpsIcons(new ArraySet<>()); + } + + @Test + public void testShowAppOpsIcons_header() throws Exception { + NotificationHeaderView mockHeader = mock(NotificationHeaderView.class); + + NotificationContentView publicLayout = mock(NotificationContentView.class); + mGroup.setPublicLayout(publicLayout); + NotificationContentView privateLayout = mock(NotificationContentView.class); + mGroup.setPrivateLayout(privateLayout); + NotificationChildrenContainer mockContainer = mock(NotificationChildrenContainer.class); + when(mockContainer.getNotificationChildCount()).thenReturn(1); + when(mockContainer.getHeaderView()).thenReturn(mockHeader); + mGroup.setChildrenContainer(mockContainer); + + ArraySet<Integer> ops = new ArraySet<>(); + ops.add(AppOpsManager.OP_ANSWER_PHONE_CALLS); + mGroup.showAppOpsIcons(ops); + + verify(mockHeader, times(1)).showAppOpsIcons(ops); + verify(privateLayout, times(1)).showAppOpsIcons(ops); + verify(publicLayout, times(1)).showAppOpsIcons(ops); + + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationContentViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationContentViewTest.java index 436849c9d700..1fb4c371a408 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationContentViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationContentViewTest.java @@ -16,14 +16,23 @@ package com.android.systemui.statusbar; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import android.app.AppOpsManager; import android.support.test.annotation.UiThreadTest; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; +import android.util.ArraySet; +import android.view.NotificationHeaderView; import android.view.View; import com.android.systemui.SysuiTestCase; @@ -75,4 +84,35 @@ public class NotificationContentViewTest extends SysuiTestCase { mView.setHeadsUpAnimatingAway(true); Assert.assertFalse(mView.isAnimatingVisibleType()); } + + @Test + @UiThreadTest + public void testShowAppOpsIcons() { + NotificationHeaderView mockContracted = mock(NotificationHeaderView.class); + when(mockContracted.findViewById(com.android.internal.R.id.notification_header)) + .thenReturn(mockContracted); + NotificationHeaderView mockExpanded = mock(NotificationHeaderView.class); + when(mockExpanded.findViewById(com.android.internal.R.id.notification_header)) + .thenReturn(mockExpanded); + NotificationHeaderView mockHeadsUp = mock(NotificationHeaderView.class); + when(mockHeadsUp.findViewById(com.android.internal.R.id.notification_header)) + .thenReturn(mockHeadsUp); + NotificationHeaderView mockAmbient = mock(NotificationHeaderView.class); + when(mockAmbient.findViewById(com.android.internal.R.id.notification_header)) + .thenReturn(mockAmbient); + + mView.setContractedChild(mockContracted); + mView.setExpandedChild(mockExpanded); + mView.setHeadsUpChild(mockHeadsUp); + mView.setAmbientChild(mockAmbient); + + ArraySet<Integer> ops = new ArraySet<>(); + ops.add(AppOpsManager.OP_ANSWER_PHONE_CALLS); + mView.showAppOpsIcons(ops); + + verify(mockContracted, times(1)).showAppOpsIcons(ops); + verify(mockExpanded, times(1)).showAppOpsIcons(ops); + verify(mockAmbient, never()).showAppOpsIcons(ops); + verify(mockHeadsUp, times(1)).showAppOpsIcons(any()); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationDataTest.java index 972eddb46901..b1e1c02a035f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationDataTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationDataTest.java @@ -16,8 +16,16 @@ package com.android.systemui.statusbar; +import static android.app.AppOpsManager.OP_ACCEPT_HANDOVER; +import static android.app.AppOpsManager.OP_CAMERA; + +import static junit.framework.Assert.assertEquals; + import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -33,7 +41,9 @@ import android.service.notification.StatusBarNotification; import android.support.test.annotation.UiThreadTest; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; +import android.util.ArraySet; +import com.android.systemui.ForegroundServiceController; import com.android.systemui.SysuiTestCase; import com.android.systemui.statusbar.phone.NotificationGroupManager; @@ -41,6 +51,8 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidJUnit4.class) @@ -51,6 +63,10 @@ public class NotificationDataTest extends SysuiTestCase { private final StatusBarNotification mMockStatusBarNotification = mock(StatusBarNotification.class); + @Mock + ForegroundServiceController mFsc; + @Mock + NotificationData.Environment mEnvironment; private final IPackageManager mMockPackageManager = mock(IPackageManager.class); private NotificationData mNotificationData; @@ -58,6 +74,7 @@ public class NotificationDataTest extends SysuiTestCase { @Before public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); when(mMockStatusBarNotification.getUid()).thenReturn(UID_NORMAL); when(mMockPackageManager.checkUidPermission( @@ -69,9 +86,11 @@ public class NotificationDataTest extends SysuiTestCase { eq(UID_ALLOW_DURING_SETUP))) .thenReturn(PackageManager.PERMISSION_GRANTED); - NotificationData.Environment mock = mock(NotificationData.Environment.class); - when(mock.getGroupManager()).thenReturn(new NotificationGroupManager()); - mNotificationData = new TestableNotificationData(mock); + mDependency.injectTestDependency(ForegroundServiceController.class, mFsc); + when(mEnvironment.getGroupManager()).thenReturn(new NotificationGroupManager()); + when(mEnvironment.isDeviceProvisioned()).thenReturn(true); + when(mEnvironment.isNotificationForCurrentProfiles(any())).thenReturn(true); + mNotificationData = new TestableNotificationData(mEnvironment); mNotificationData.updateRanking(mock(NotificationListenerService.RankingMap.class)); mRow = new NotificationTestHelper(getContext()).createRow(); } @@ -117,6 +136,117 @@ public class NotificationDataTest extends SysuiTestCase { Assert.assertTrue(mRow.getEntry().channel != null); } + @Test + public void testAdd_appOpsAdded() { + ArraySet<Integer> expected = new ArraySet<>(); + expected.add(3); + expected.add(235); + expected.add(1); + when(mFsc.getAppOps(mRow.getEntry().notification.getUserId(), + mRow.getEntry().notification.getPackageName())).thenReturn(expected); + + mNotificationData.add(mRow.getEntry()); + assertEquals(expected.size(), + mNotificationData.get(mRow.getEntry().key).mActiveAppOps.size()); + for (int op : expected) { + assertTrue(" entry missing op " + op, + mNotificationData.get(mRow.getEntry().key).mActiveAppOps.contains(op)); + } + } + + @Test + public void testAdd_noExistingAppOps() { + when(mFsc.getAppOps(mRow.getEntry().notification.getUserId(), + mRow.getEntry().notification.getPackageName())).thenReturn(null); + + mNotificationData.add(mRow.getEntry()); + assertEquals(0, mNotificationData.get(mRow.getEntry().key).mActiveAppOps.size()); + } + + @Test + public void testAllRelevantNotisTaggedWithAppOps() throws Exception { + mNotificationData.add(mRow.getEntry()); + ExpandableNotificationRow row2 = new NotificationTestHelper(getContext()).createRow(); + mNotificationData.add(row2.getEntry()); + ExpandableNotificationRow diffPkg = + new NotificationTestHelper(getContext()).createRow("pkg", 4000); + mNotificationData.add(diffPkg.getEntry()); + + ArraySet<Integer> expectedOps = new ArraySet<>(); + expectedOps.add(OP_CAMERA); + expectedOps.add(OP_ACCEPT_HANDOVER); + + for (int op : expectedOps) { + mNotificationData.updateAppOp(op, NotificationTestHelper.UID, + NotificationTestHelper.PKG, true); + } + for (int op : expectedOps) { + assertTrue(mRow.getEntry().key + " doesn't have op " + op, + mNotificationData.get(mRow.getEntry().key).mActiveAppOps.contains(op)); + assertTrue(row2.getEntry().key + " doesn't have op " + op, + mNotificationData.get(row2.getEntry().key).mActiveAppOps.contains(op)); + assertFalse(diffPkg.getEntry().key + " has op " + op, + mNotificationData.get(diffPkg.getEntry().key).mActiveAppOps.contains(op)); + } + } + + @Test + public void testAppOpsRemoval() throws Exception { + mNotificationData.add(mRow.getEntry()); + ExpandableNotificationRow row2 = new NotificationTestHelper(getContext()).createRow(); + mNotificationData.add(row2.getEntry()); + + ArraySet<Integer> expectedOps = new ArraySet<>(); + expectedOps.add(OP_CAMERA); + expectedOps.add(OP_ACCEPT_HANDOVER); + + for (int op : expectedOps) { + mNotificationData.updateAppOp(op, NotificationTestHelper.UID, + NotificationTestHelper.PKG, true); + } + + expectedOps.remove(OP_ACCEPT_HANDOVER); + mNotificationData.updateAppOp(OP_ACCEPT_HANDOVER, NotificationTestHelper.UID, + NotificationTestHelper.PKG, false); + + assertTrue(mRow.getEntry().key + " doesn't have op " + OP_CAMERA, + mNotificationData.get(mRow.getEntry().key).mActiveAppOps.contains(OP_CAMERA)); + assertTrue(row2.getEntry().key + " doesn't have op " + OP_CAMERA, + mNotificationData.get(row2.getEntry().key).mActiveAppOps.contains(OP_CAMERA)); + assertFalse(mRow.getEntry().key + " has op " + OP_ACCEPT_HANDOVER, + mNotificationData.get(mRow.getEntry().key) + .mActiveAppOps.contains(OP_ACCEPT_HANDOVER)); + assertFalse(row2.getEntry().key + " has op " + OP_ACCEPT_HANDOVER, + mNotificationData.get(row2.getEntry().key) + .mActiveAppOps.contains(OP_ACCEPT_HANDOVER)); + } + + @Test + public void testSuppressSystemAlertNotification() { + when(mFsc.isSystemAlertWarningNeeded(anyInt(), anyString())).thenReturn(false); + when(mFsc.isSystemAlertNotification(any())).thenReturn(true); + + assertTrue(mNotificationData.shouldFilterOut(mRow.getEntry().notification)); + } + + @Test + public void testDoNotSuppressSystemAlertNotification() { + when(mFsc.isSystemAlertWarningNeeded(anyInt(), anyString())).thenReturn(true); + when(mFsc.isSystemAlertNotification(any())).thenReturn(true); + + assertFalse(mNotificationData.shouldFilterOut(mRow.getEntry().notification)); + + when(mFsc.isSystemAlertWarningNeeded(anyInt(), anyString())).thenReturn(true); + when(mFsc.isSystemAlertNotification(any())).thenReturn(false); + + assertFalse(mNotificationData.shouldFilterOut(mRow.getEntry().notification)); + + when(mFsc.isSystemAlertWarningNeeded(anyInt(), anyString())).thenReturn(false); + when(mFsc.isSystemAlertNotification(any())).thenReturn(false); + + assertFalse(mNotificationData.shouldFilterOut(mRow.getEntry().notification)); + } + private void initStatusBarNotification(boolean allowDuringSetup) { Bundle bundle = new Bundle(); bundle.putBoolean(Notification.EXTRA_ALLOW_DURING_SETUP, allowDuringSetup); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationEntryManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationEntryManagerTest.java index f9ec3f92181f..37dd939ea70a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationEntryManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationEntryManagerTest.java @@ -23,14 +23,17 @@ import static junit.framework.Assert.assertTrue; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActivityManager; +import android.app.AppOpsManager; import android.app.Notification; import android.app.NotificationManager; import android.content.Context; @@ -274,4 +277,40 @@ public class NotificationEntryManagerTest extends SysuiTestCase { assertNull(mEntryManager.getNotificationData().get(mSbn.getKey())); } + + @Test + public void testUpdateAppOps_foregroundNoti() { + com.android.systemui.util.Assert.isNotMainThread(); + + when(mForegroundServiceController.getStandardLayoutKey(anyInt(), anyString())) + .thenReturn("something"); + mEntry.row = mRow; + mEntryManager.getNotificationData().add(mEntry); + + + mHandler.post(() -> { + mEntryManager.updateNotificationsForAppOps( + AppOpsManager.OP_CAMERA, mEntry.notification.getUid(), + mEntry.notification.getPackageName(), true); + }); + waitForIdleSync(mHandler); + + verify(mPresenter, times(1)).updateNotificationViews(); + assertTrue(mEntryManager.getNotificationData().get(mEntry.key).mActiveAppOps.contains( + AppOpsManager.OP_CAMERA)); + } + + @Test + public void testUpdateAppOps_otherNoti() { + com.android.systemui.util.Assert.isNotMainThread(); + + when(mForegroundServiceController.getStandardLayoutKey(anyInt(), anyString())) + .thenReturn(null); + mHandler.post(() -> { + mEntryManager.updateNotificationsForAppOps(AppOpsManager.OP_CAMERA, 1000, "pkg", true); + }); + waitForIdleSync(mHandler); + + verify(mPresenter, never()).updateNotificationViews(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationTestHelper.java index f3c1171f650c..27642544c129 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationTestHelper.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationTestHelper.java @@ -48,6 +48,8 @@ public class NotificationTestHelper { private ExpandableNotificationRow mRow; private InflationException mException; private HeadsUpManager mHeadsUpManager; + protected static final String PKG = "com.android.systemui"; + protected static final int UID = 1000; public NotificationTestHelper(Context context) { mContext = context; @@ -55,7 +57,7 @@ public class NotificationTestHelper { mHeadsUpManager = new HeadsUpManagerPhone(mContext, null, mGroupManager, null, null); } - public ExpandableNotificationRow createRow() throws Exception { + public ExpandableNotificationRow createRow(String pkg, int uid) throws Exception { Notification publicVersion = new Notification.Builder(mContext).setSmallIcon( R.drawable.ic_person) .setCustomContentView(new RemoteViews(mContext.getPackageName(), @@ -67,10 +69,19 @@ public class NotificationTestHelper { .setContentText("Text") .setPublicVersion(publicVersion) .build(); - return createRow(notification); + return createRow(notification, pkg, uid); + } + + public ExpandableNotificationRow createRow() throws Exception { + return createRow(PKG, UID); } public ExpandableNotificationRow createRow(Notification notification) throws Exception { + return createRow(notification, PKG, UID); + } + + public ExpandableNotificationRow createRow(Notification notification, String pkg, int uid) + throws Exception { LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( mContext.LAYOUT_INFLATER_SERVICE); mInstrumentation.runOnMainSync(() -> { @@ -83,8 +94,7 @@ public class NotificationTestHelper { row.setHeadsUpManager(mHeadsUpManager); row.setAboveShelfChangedListener(aboveShelf -> {}); UserHandle mUser = UserHandle.of(ActivityManager.getCurrentUser()); - StatusBarNotification sbn = new StatusBarNotification("com.android.systemui", - "com.android.systemui", mId++, null, 1000, + StatusBarNotification sbn = new StatusBarNotification(pkg, pkg, mId++, null, uid, 2000, notification, mUser, null, System.currentTimeMillis()); NotificationData.Entry entry = new NotificationData.Entry(sbn); entry.row = row; 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 fbe730a64c6f..76ed45206dff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java @@ -19,6 +19,9 @@ package com.android.systemui.statusbar; import static junit.framework.Assert.assertTrue; import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -170,6 +173,19 @@ public class NotificationViewHierarchyManagerTest extends SysuiTestCase { assertEquals(View.VISIBLE, entry1.row.getVisibility()); } + @Test + public void testUpdateNotificationViews_appOps() throws Exception { + NotificationData.Entry entry0 = createEntry(); + entry0.row = spy(entry0.row); + when(mNotificationData.getActiveNotifications()).thenReturn( + Lists.newArrayList(entry0)); + mListContainer.addContainerView(entry0.row); + + mViewHierarchyManager.updateNotificationViews(); + + verify(entry0.row, times(1)).showAppOpsIcons(any()); + } + private class FakeListContainer implements NotificationListContainer { final LinearLayout mLayout = new LinearLayout(mContext); final List<View> mRows = Lists.newArrayList(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java index 31442af5a04c..ff545f0bd653 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java @@ -69,6 +69,7 @@ import com.android.systemui.assist.AssistManager; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.recents.misc.SystemServicesProxy; import com.android.systemui.statusbar.ActivatableNotificationView; +import com.android.systemui.statusbar.AppOpsListener; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.KeyguardIndicationController; import com.android.systemui.statusbar.NotificationData; @@ -145,6 +146,7 @@ public class StatusBarTest extends SysuiTestCase { mDependency.injectTestDependency(VisualStabilityManager.class, mVisualStabilityManager); mDependency.injectTestDependency(NotificationListener.class, mNotificationListener); mDependency.injectTestDependency(KeyguardMonitor.class, mock(KeyguardMonitorImpl.class)); + mDependency.injectTestDependency(AppOpsListener.class, mock(AppOpsListener.class)); mContext.addMockSystemService(TrustManager.class, mock(TrustManager.class)); mContext.addMockSystemService(FingerprintManager.class, mock(FingerprintManager.class)); |