diff --git a/core/java/android/view/NotificationHeaderView.java b/core/java/android/view/NotificationHeaderView.java index 137e820805e21..7a10364708941 100644 --- a/core/java/android/view/NotificationHeaderView.java +++ b/core/java/android/view/NotificationHeaderView.java @@ -51,6 +51,7 @@ public class NotificationHeaderView extends ViewGroup { private View mHeaderText; private View mSecondaryHeaderText; private OnClickListener mExpandClickListener; + private OnClickListener mAppOpsListener; private HeaderTouchListener mTouchListener = new HeaderTouchListener(); private ImageView mExpandButton; private CachingIconView mIcon; @@ -267,15 +268,26 @@ public class NotificationHeaderView extends ViewGroup { } private void updateTouchListener() { - if (mExpandClickListener != null) { - mTouchListener.bindTouchRects(); + 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; + mAppOps.setOnClickListener(mAppOpsListener); + updateTouchListener(); } @Override public void setOnClickListener(@Nullable OnClickListener l) { mExpandClickListener = l; - setOnTouchListener(mExpandClickListener != null ? mTouchListener : null); mExpandButton.setOnClickListener(mExpandClickListener); updateTouchListener(); } @@ -308,7 +320,7 @@ public class NotificationHeaderView extends ViewGroup { * Shows or hides 'app op in use' icons based on app usage. */ public void showAppOpsIcons(ArraySet appOps) { - if (mOverlayIcon == null || mCameraIcon == null || mMicIcon == null) { + if (mOverlayIcon == null || mCameraIcon == null || mMicIcon == null || appOps == null) { return; } @@ -366,6 +378,7 @@ public class NotificationHeaderView extends ViewGroup { private final ArrayList mTouchRects = new ArrayList<>(); private Rect mExpandButtonRect; + private Rect mAppOpsRect; private int mTouchSlop; private boolean mTrackGesture; private float mDownX; @@ -378,6 +391,7 @@ public class NotificationHeaderView extends ViewGroup { mTouchRects.clear(); addRectAroundView(mIcon); mExpandButtonRect = addRectAroundView(mExpandButton); + mAppOpsRect = addRectAroundView(mAppOps); addWidthRect(); mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); } @@ -399,16 +413,18 @@ public class NotificationHeaderView extends ViewGroup { private Rect getRectAroundView(View view) { float size = 48 * getResources().getDisplayMetrics().density; + float width = Math.max(size, view.getWidth()); + float height = Math.max(size, view.getHeight()); final Rect r = new Rect(); if (view.getVisibility() == GONE) { view = getFirstChildNotGone(); - r.left = (int) (view.getLeft() - size / 2.0f); + r.left = (int) (view.getLeft() - width / 2.0f); } else { - r.left = (int) ((view.getLeft() + view.getRight()) / 2.0f - size / 2.0f); + r.left = (int) ((view.getLeft() + view.getRight()) / 2.0f - width / 2.0f); } - r.top = (int) ((view.getTop() + view.getBottom()) / 2.0f - size / 2.0f); - r.bottom = (int) (r.top + size); - r.right = (int) (r.left + size); + r.top = (int) ((view.getTop() + view.getBottom()) / 2.0f - height / 2.0f); + r.bottom = (int) (r.top + height); + r.right = (int) (r.left + width); return r; } @@ -436,6 +452,11 @@ public class NotificationHeaderView extends ViewGroup { break; case MotionEvent.ACTION_UP: if (mTrackGesture) { + if (mAppOps.isVisibleToUser() && (mAppOpsRect.contains((int) x, (int) y) + || mAppOpsRect.contains((int) mDownX, (int) mDownY))) { + mAppOps.performClick(); + return true; + } mExpandButton.performClick(); } break; diff --git a/core/res/res/layout/notification_template_header.xml b/core/res/res/layout/notification_template_header.xml index c03cf51d6bcad..3196d000f191a 100644 --- a/core/res/res/layout/notification_template_header.xml +++ b/core/res/res/layout/notification_template_header.xml @@ -129,8 +129,9 @@ items); public void setMenuClickListener(OnMenuEventListener listener); diff --git a/packages/SystemUI/res/layout/app_ops_info.xml b/packages/SystemUI/res/layout/app_ops_info.xml new file mode 100644 index 0000000000000..74a4c6e3dee45 --- /dev/null +++ b/packages/SystemUI/res/layout/app_ops_info.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index c89d818bdbfd4..4420103f14fe0 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1545,6 +1545,25 @@ These notifications can\'t be turned off + camera + + microphone + + displaying over other apps on your screen + + + This app is %1$s. + This app is %1$s and %2$s. + + + + using the %1$s + using the %1$s and %2$s + + + Settings + Ok + Notification controls for %1$s opened diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AppOpsInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/AppOpsInfo.java new file mode 100644 index 0000000000000..322a529d5e1ac --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/AppOpsInfo.java @@ -0,0 +1,213 @@ +/* + * 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 android.app.AppOpsManager; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.os.RemoteException; +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.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 mAppOps; + private MetricsLogger mMetricsLogger; + private OnSettingsClickListener mOnSettingsClickListener; + private NotificationGuts mGutsContainer; + + private OnClickListener mOnOk = v -> { + closeControls(v); + }; + + public AppOpsInfo(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public interface OnSettingsClickListener { + void onClick(View v, String pkg, int uid, ArraySet ops); + } + + public void bindGuts(final PackageManager pm, + final OnSettingsClickListener onSettingsClick, + final StatusBarNotification sbn, + ArraySet activeOps) { + mPkg = sbn.getPackageName(); + mSbn = sbn; + mPm = pm; + mAppName = mPkg; + mOnSettingsClickListener = onSettingsClick; + mAppOps = activeOps; + + bindHeader(); + bindPrompt(); + bindButtons(); + + 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(getPromptString()); + } + + 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); + } + + private String getPromptString() { + String cameraString = + mContext.getResources().getString(R.string.notification_appops_camera_active); + String micString = + mContext.getResources().getString(R.string.notification_appops_microphone_active); + String overlayString = + mContext.getResources().getString(R.string.notification_appops_overlay_active); + String using = null; + String promptString; + if (mAppOps.contains(AppOpsManager.OP_CAMERA) + && mAppOps.contains(AppOpsManager.OP_RECORD_AUDIO)) { + using = mContext.getResources().getQuantityString( + R.plurals.notification_using, 2, micString, cameraString); + } else if (mAppOps.contains(AppOpsManager.OP_CAMERA)) { + using = mContext.getResources().getQuantityString( + R.plurals.notification_using, 1, cameraString); + } else if (mAppOps.contains(AppOpsManager.OP_RECORD_AUDIO)){ + using = mContext.getResources().getQuantityString( + R.plurals.notification_using, 1, micString); + } + + if (mAppOps.contains(AppOpsManager.OP_SYSTEM_ALERT_WINDOW)) { + if (using != null) { + promptString = mContext.getResources().getQuantityString( + R.plurals.notification_appops, 2, overlayString, using); + } else { + promptString = mContext.getResources().getQuantityString( + R.plurals.notification_appops, 1, overlayString); + } + } else { + promptString = mContext.getResources().getQuantityString( + R.plurals.notification_appops, 1, using); + } + + return promptString; + } + + @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)); + } + } + } + + private void closeControls(View v) { + mMetricsLogger.visibility(MetricsEvent.APP_OPS_GUTS, false); + int[] parentLoc = new int[2]; + int[] targetLoc = new int[2]; + mGutsContainer.getLocationOnScreen(parentLoc); + v.getLocationOnScreen(targetLoc); + final int centerX = v.getWidth() / 2; + final int centerY = v.getHeight() / 2; + final int x = targetLoc[0] - parentLoc[0] + centerX; + final int y = targetLoc[1] - parentLoc[1] + centerY; + mGutsContainer.closeControls(x, y, false, false); + } + + @Override + public void setGutsParent(NotificationGuts guts) { + mGutsContainer = guts; + } + + @Override + public boolean willBeRemoved() { + return false; + } + + @Override + public View getContentView() { + return this; + } + + @Override + public boolean handleCloseControls(boolean save, boolean force) { + return false; + } + + @Override + public int getActualHeight() { + return getHeight(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java index 785fc1cc59228..e5c5dcdb0f586 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java @@ -25,8 +25,8 @@ import android.animation.ObjectAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.annotation.Nullable; import android.content.Context; -import android.content.res.Resources; import android.content.res.Configuration; +import android.content.res.Resources; import android.graphics.Path; import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.AnimationDrawable; @@ -188,6 +188,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private boolean mShowNoBackground; private ExpandableNotificationRow mNotificationParent; private OnExpandClickListener mOnExpandClickListener; + private View.OnClickListener mOnAppOpsClickListener; // Listener will be called when receiving a long click event. // Use #setLongPressPosition to optionally assign positional data with the long press. @@ -890,6 +891,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView ArrayList items = new ArrayList<>(); items.add(NotificationMenuRow.createInfoItem(mContext)); items.add(NotificationMenuRow.createSnoozeItem(mContext)); + items.add(NotificationMenuRow.createAppOpsItem(mContext)); mMenuRow.setMenuItems(items); } if (existed) { @@ -1363,6 +1365,20 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mPublicLayout.showAppOpsIcons(activeOps); } + public View.OnClickListener getAppOpsOnClickListener() { + return mOnAppOpsClickListener; + } + + protected void setAppOpsOnClickListener(ExpandableNotificationRow.OnAppOpsClickListener l) { + mOnAppOpsClickListener = v -> { + createMenu(); + MenuItem menuItem = getProvider().getAppOpsMenuItem(mContext); + if (menuItem != null) { + l.onClick(this, v.getWidth() / 2, v.getHeight() / 2, menuItem); + } + }; + } + @Override protected void onFinishInflate() { super.onFinishInflate(); @@ -2658,4 +2674,15 @@ public class ExpandableNotificationRow extends ActivatableNotificationView */ boolean onLongPress(View v, int x, int y, MenuItem item); } + + /** + * Equivalent to View.OnClickListener with coordinates + */ + public interface OnAppOpsClickListener { + /** + * Equivalent to {@link View.OnClickListener#onClick(View)} with coordinates + * @return whether the click was handled + */ + boolean onClick(View v, int x, int y, MenuItem item); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationEntryManager.java index 71f7911b41f1c..146de008b6108 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationEntryManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationEntryManager.java @@ -124,6 +124,7 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater. protected boolean mUseHeadsUp = false; protected boolean mDisableNotificationAlerts; protected NotificationListContainer mListContainer; + private ExpandableNotificationRow.OnAppOpsClickListener mOnAppOpsClickListener; private final class NotificationClicker implements View.OnClickListener { @@ -271,6 +272,7 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater. mDeviceProvisionedController.addCallback(mDeviceProvisionedListener); mHeadsUpObserver.onChange(true); // set up + mOnAppOpsClickListener = mGutsManager::openGuts; } public NotificationData getNotificationData() { @@ -361,6 +363,8 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater. row.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); } + row.setAppOpsOnClickListener(mOnAppOpsClickListener); + mCallback.onBindRow(entry, pmUser, sbn, row); } @@ -622,7 +626,7 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater. } } - private void updateNotification(NotificationData.Entry entry, PackageManager pmUser, + protected void updateNotification(NotificationData.Entry entry, PackageManager pmUser, StatusBarNotification sbn, ExpandableNotificationRow row) { row.setNeedsRedaction(mLockscreenUserManager.needsRedaction(entry)); boolean isLowPriority = mNotificationData.isAmbient(sbn.getKey()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGutsManager.java index f730601ac919b..9b2f939f4df44 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGutsManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGutsManager.java @@ -15,9 +15,13 @@ */ package com.android.systemui.statusbar; +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 android.service.notification.NotificationListenerService.Ranking .USER_SENTIMENT_NEGATIVE; +import android.app.AppOpsManager; import android.app.INotificationManager; import android.app.NotificationChannel; import android.content.Context; @@ -147,6 +151,23 @@ public class NotificationGutsManager implements Dumpable { mPresenter.startNotificationGutsIntent(intent, appUid, row); } + protected void startAppOpsSettingsActivity(String pkg, int uid, ArraySet ops, + ExpandableNotificationRow row) { + if (ops.contains(OP_SYSTEM_ALERT_WINDOW)) { + if (ops.contains(OP_CAMERA) || ops.contains(OP_RECORD_AUDIO)) { + startAppNotificationSettingsActivity(pkg, uid, null, row); + } else { + Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); + intent.setData(Uri.fromParts("package", pkg, null)); + mPresenter.startNotificationGutsIntent(intent, uid, row); + } + } else if (ops.contains(OP_CAMERA) || ops.contains(OP_RECORD_AUDIO)) { + Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS); + intent.putExtra(Intent.EXTRA_PACKAGE_NAME, pkg); + mPresenter.startNotificationGutsIntent(intent, uid, row); + } + } + public void bindGuts(final ExpandableNotificationRow row) { bindGuts(row, mGutsMenuItem); } @@ -185,6 +206,22 @@ public class NotificationGutsManager implements Dumpable { }); } + if (gutsView instanceof AppOpsInfo) { + AppOpsInfo info = (AppOpsInfo) gutsView; + final UserHandle userHandle = sbn.getUser(); + PackageManager pmUser = StatusBar.getPackageManagerForUser(mContext, + userHandle.getIdentifier()); + final AppOpsInfo.OnSettingsClickListener onSettingsClick = (View v, + String pkg, int uid, ArraySet ops) -> { + mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_OPS_GUTS_SETTINGS); + guts.resetFalsingCheck(); + startAppOpsSettingsActivity(pkg, uid, ops, row); + }; + if (!row.getEntry().mActiveAppOps.isEmpty()) { + info.bindGuts(pmUser, onSettingsClick, sbn, row.getEntry().mActiveAppOps); + } + } + if (gutsView instanceof NotificationInfo) { final UserHandle userHandle = sbn.getUser(); PackageManager pmUser = StatusBar.getPackageManagerForUser(mContext, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMenuRow.java index 037eeb2d298bb..0eb6bd4e68e80 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMenuRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMenuRow.java @@ -30,6 +30,7 @@ import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; +import android.annotation.Nullable; import android.app.Notification; import android.content.Context; import android.content.res.Resources; @@ -68,6 +69,7 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl private Context mContext; private FrameLayout mMenuContainer; private MenuItem mInfoItem; + private MenuItem mAppOpsItem; private ArrayList mMenuItems; private OnMenuEventListener mMenuListener; @@ -120,6 +122,11 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl return mInfoItem; } + @Override + public MenuItem getAppOpsMenuItem(Context context) { + return mAppOpsItem; + } + @Override public void setSwipeActionHelper(NotificationSwipeActionHelper helper) { mSwipeHelper = helper; @@ -189,6 +196,9 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl mInfoItem = createInfoItem(mContext); mMenuItems.add(mInfoItem); + mAppOpsItem = createAppOpsItem(mContext); + mMenuItems.add(mAppOpsItem); + // Construct the menu views if (mMenuContainer != null) { mMenuContainer.removeAllViews(); @@ -614,6 +624,14 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl return info; } + public 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) { @@ -631,22 +649,29 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl GutsContent mGutsContent; String mContentDescription; + /** + * Add a new 'guts' panel. If iconResId < 0 it will not appear in the slow swipe menu + * but can still be exposed via other affordances. + */ public NotificationMenuItem(Context context, String s, GutsContent content, int iconResId) { Resources res = context.getResources(); int padding = res.getDimensionPixelSize(R.dimen.notification_menu_icon_padding); int tint = res.getColor(R.color.notification_gear_color); - AlphaOptimizedImageView iv = new AlphaOptimizedImageView(context); - iv.setPadding(padding, padding, padding, padding); - Drawable icon = context.getResources().getDrawable(iconResId); - iv.setImageDrawable(icon); - iv.setColorFilter(tint); - iv.setAlpha(1f); - mMenuView = iv; + if (iconResId >= 0) { + AlphaOptimizedImageView iv = new AlphaOptimizedImageView(context); + iv.setPadding(padding, padding, padding, padding); + Drawable icon = context.getResources().getDrawable(iconResId); + iv.setImageDrawable(icon); + iv.setColorFilter(tint); + iv.setAlpha(1f); + mMenuView = iv; + } mContentDescription = s; mGutsContent = content; } @Override + @Nullable public View getMenuView() { return mMenuView; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationHeaderViewWrapper.java index b95b8a392c866..dfcd5e60719ad 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationHeaderViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationHeaderViewWrapper.java @@ -98,6 +98,7 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { }, TRANSFORMING_VIEW_TITLE); resolveHeaderViews(); updateInvertHelper(); + addAppOpsOnClickListener(row); } @Override @@ -121,6 +122,10 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { getDozer().setColor(mColor); } + private void addAppOpsOnClickListener(ExpandableNotificationRow row) { + mNotificationHeader.setAppOpsOnClickListener(row.getAppOpsOnClickListener()); + } + private int resolveColor(ImageView icon) { if (icon != null && icon.getDrawable() != null) { ColorFilter filter = icon.getDrawable().getColorFilter(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/AppOpsInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/AppOpsInfoTest.java new file mode 100644 index 0000000000000..660d2dc4a1562 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/AppOpsInfoTest.java @@ -0,0 +1,220 @@ +/* + * 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 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.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; + + @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, 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, new ArraySet<>()); + final ImageView iconView = mAppOpsInfo.findViewById(R.id.pkgicon); + assertEquals(iconDrawable, iconView.getDrawable()); + } + + @Test + public void testBindNotification_SetsOnClickListenerForSettings() throws Exception { + ArraySet expectedOps = new ArraySet<>(); + expectedOps.add(OP_CAMERA); + final CountDownLatch latch = new CountDownLatch(1); + mAppOpsInfo.bindGuts(mMockPackageManager, (View v, String pkg, int uid, + ArraySet ops) -> { + assertEquals(TEST_PACKAGE_NAME, pkg); + assertEquals(expectedOps, ops); + assertEquals(TEST_UID, uid); + latch.countDown(); + }, mSbn, expectedOps); + + final View settingsButton = mAppOpsInfo.findViewById(R.id.settings); + settingsButton.performClick(); + // Verify that listener was triggered. + assertEquals(0, latch.getCount()); + } + + @Test + public void testOk() { + ArraySet expectedOps = new ArraySet<>(); + expectedOps.add(OP_CAMERA); + final CountDownLatch latch = new CountDownLatch(1); + mAppOpsInfo.bindGuts(mMockPackageManager, (View v, String pkg, int uid, + ArraySet ops) -> { + assertEquals(TEST_PACKAGE_NAME, pkg); + assertEquals(expectedOps, ops); + assertEquals(TEST_UID, uid); + latch.countDown(); + }, mSbn, expectedOps); + + final View okButton = mAppOpsInfo.findViewById(R.id.ok); + okButton.performClick(); + assertEquals(1, latch.getCount()); + verify(mGutsParent, times(1)).closeControls(anyInt(), anyInt(), anyBoolean(), anyBoolean()); + } + + @Test + public void testPrompt_camera() { + ArraySet expectedOps = new ArraySet<>(); + expectedOps.add(OP_CAMERA); + mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, expectedOps); + TextView prompt = mAppOpsInfo.findViewById(R.id.prompt); + assertEquals("This app is using the camera.", prompt.getText()); + } + + @Test + public void testPrompt_mic() { + ArraySet expectedOps = new ArraySet<>(); + expectedOps.add(OP_RECORD_AUDIO); + mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, expectedOps); + TextView prompt = mAppOpsInfo.findViewById(R.id.prompt); + assertEquals("This app is using the microphone.", prompt.getText()); + } + + @Test + public void testPrompt_overlay() { + ArraySet expectedOps = new ArraySet<>(); + expectedOps.add(OP_SYSTEM_ALERT_WINDOW); + mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, 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 expectedOps = new ArraySet<>(); + expectedOps.add(OP_CAMERA); + expectedOps.add(OP_RECORD_AUDIO); + mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, 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 expectedOps = new ArraySet<>(); + expectedOps.add(OP_CAMERA); + expectedOps.add(OP_RECORD_AUDIO); + expectedOps.add(OP_SYSTEM_ALERT_WINDOW); + mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, 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 expectedOps = new ArraySet<>(); + expectedOps.add(OP_CAMERA); + expectedOps.add(OP_SYSTEM_ALERT_WINDOW); + mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, 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 expectedOps = new ArraySet<>(); + expectedOps.add(OP_RECORD_AUDIO); + expectedOps.add(OP_SYSTEM_ALERT_WINDOW); + mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, 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/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ExpandableNotificationRowTest.java index ce629bb41e7b5..34e444e9ad18a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ExpandableNotificationRowTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ExpandableNotificationRowTest.java @@ -17,6 +17,7 @@ package com.android.systemui.statusbar; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -153,14 +154,15 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test - public void testShowAppOpsIcons_noHeader() { + public void testShowAppOps_noHeader() { // public notification is custom layout - no header mGroup.setSensitive(true, true); - mGroup.showAppOpsIcons(new ArraySet<>()); + mGroup.setAppOpsOnClickListener(null); + mGroup.showAppOpsIcons(null); } @Test - public void testShowAppOpsIcons_header() throws Exception { + public void testShowAppOpsIcons_header() { NotificationHeaderView mockHeader = mock(NotificationHeaderView.class); NotificationContentView publicLayout = mock(NotificationContentView.class); @@ -181,4 +183,16 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { verify(publicLayout, times(1)).showAppOpsIcons(ops); } + + @Test + public void testAppOpsOnClick() { + ExpandableNotificationRow.OnAppOpsClickListener l = mock( + ExpandableNotificationRow.OnAppOpsClickListener.class); + View view = mock(View.class); + + mGroup.setAppOpsOnClickListener(l); + + mGroup.getAppOpsOnClickListener().onClick(view); + verify(l, times(1)).onClick(any(), anyInt(), anyInt(), any()); + } } 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 37dd939ea70a2..7e5db344629c3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationEntryManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationEntryManagerTest.java @@ -37,6 +37,8 @@ import android.app.AppOpsManager; import android.app.Notification; import android.app.NotificationManager; import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.os.Bundle; import android.os.Handler; import android.os.Looper; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationGutsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationGutsManagerTest.java index 8f38c2ca948d7..6209d59993c32 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationGutsManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationGutsManagerTest.java @@ -16,6 +16,10 @@ package com.android.systemui.statusbar; +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.assertNotNull; import static org.junit.Assert.assertEquals; @@ -34,14 +38,17 @@ import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; +import android.content.Intent; import android.os.Binder; import android.os.Handler; import android.os.Looper; import android.os.UserHandle; +import android.provider.Settings; import android.service.notification.StatusBarNotification; import android.support.test.filters.SmallTest; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.util.ArraySet; import android.view.LayoutInflater; import android.view.View; @@ -58,6 +65,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoRule; import org.mockito.junit.MockitoJUnit; @@ -164,6 +172,81 @@ public class NotificationGutsManagerTest extends SysuiTestCase { verify(row, times(2)).setGutsView(any()); } + @Test + public void testAppOpsSettingsIntent_camera() { + ArraySet ops = new ArraySet<>(); + ops.add(OP_CAMERA); + mGutsManager.startAppOpsSettingsActivity("", 0, ops, null); + ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); + verify(mPresenter, times(1)).startNotificationGutsIntent(captor.capture(), anyInt(), any()); + assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.getValue().getAction()); + } + + @Test + public void testAppOpsSettingsIntent_mic() { + ArraySet ops = new ArraySet<>(); + ops.add(OP_RECORD_AUDIO); + mGutsManager.startAppOpsSettingsActivity("", 0, ops, null); + ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); + verify(mPresenter, times(1)).startNotificationGutsIntent(captor.capture(), anyInt(), any()); + assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.getValue().getAction()); + } + + @Test + public void testAppOpsSettingsIntent_camera_mic() { + ArraySet ops = new ArraySet<>(); + ops.add(OP_CAMERA); + ops.add(OP_RECORD_AUDIO); + mGutsManager.startAppOpsSettingsActivity("", 0, ops, null); + ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); + verify(mPresenter, times(1)).startNotificationGutsIntent(captor.capture(), anyInt(), any()); + assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.getValue().getAction()); + } + + @Test + public void testAppOpsSettingsIntent_overlay() { + ArraySet ops = new ArraySet<>(); + ops.add(OP_SYSTEM_ALERT_WINDOW); + mGutsManager.startAppOpsSettingsActivity("", 0, ops, null); + ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); + verify(mPresenter, times(1)).startNotificationGutsIntent(captor.capture(), anyInt(), any()); + assertEquals(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, captor.getValue().getAction()); + } + + @Test + public void testAppOpsSettingsIntent_camera_mic_overlay() { + ArraySet ops = new ArraySet<>(); + ops.add(OP_CAMERA); + ops.add(OP_RECORD_AUDIO); + ops.add(OP_SYSTEM_ALERT_WINDOW); + mGutsManager.startAppOpsSettingsActivity("", 0, ops, null); + ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); + verify(mPresenter, times(1)).startNotificationGutsIntent(captor.capture(), anyInt(), any()); + assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.getValue().getAction()); + } + + @Test + public void testAppOpsSettingsIntent_camera_overlay() { + ArraySet ops = new ArraySet<>(); + ops.add(OP_CAMERA); + ops.add(OP_SYSTEM_ALERT_WINDOW); + mGutsManager.startAppOpsSettingsActivity("", 0, ops, null); + ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); + verify(mPresenter, times(1)).startNotificationGutsIntent(captor.capture(), anyInt(), any()); + assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.getValue().getAction()); + } + + @Test + public void testAppOpsSettingsIntent_mic_overlay() { + ArraySet ops = new ArraySet<>(); + ops.add(OP_RECORD_AUDIO); + ops.add(OP_SYSTEM_ALERT_WINDOW); + mGutsManager.startAppOpsSettingsActivity("", 0, ops, null); + ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); + verify(mPresenter, times(1)).startNotificationGutsIntent(captor.capture(), anyInt(), any()); + assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.getValue().getAction()); + } + //////////////////////////////////////////////////////////////////////////////////////////////// // Utility methods: diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationMenuRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationMenuRowTest.java index 2a2acabc8cee8..2a5a1eea2b762 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationMenuRowTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationMenuRowTest.java @@ -14,14 +14,23 @@ package com.android.systemui.statusbar; +import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.app.Notification; +import android.service.notification.StatusBarNotification; import android.support.test.filters.SmallTest; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; import android.testing.ViewUtils; import android.testing.ViewUtils; +import android.view.View; +import android.view.ViewGroup; + import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.utils.leaks.LeakCheckedTest; @@ -63,4 +72,19 @@ public class NotificationMenuRowTest extends LeakCheckedTest { NotificationMenuRowPlugin row = new NotificationMenuRow(mContext); row.resetMenu(); } + + @Test + public void testNoAppOpsInSlowSwipe() { + NotificationMenuRow row = new NotificationMenuRow(mContext); + Notification n = mock(Notification.class); + StatusBarNotification sbn = mock(StatusBarNotification.class); + when(sbn.getNotification()).thenReturn(n); + ExpandableNotificationRow parent = mock(ExpandableNotificationRow.class); + when(parent.getStatusBarNotification()).thenReturn(sbn); + row.createMenu(parent, null); + + ViewGroup container = (ViewGroup) row.getMenuView(); + // one for snooze and one for noti blocking + assertEquals(2, container.getChildCount()); + } }