Show app ops guts when tap app ops icons

Test: runtest systemui
Bug: 64085448
Change-Id: Ia51044f77311ab705ffb9dd1ae204d0131316396
This commit is contained in:
Julia Reynolds
2018-02-28 16:31:35 -05:00
parent 292d895fb2
commit b586745d86
16 changed files with 811 additions and 23 deletions

View File

@@ -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<Integer> 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<Rect> 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;

View File

@@ -129,8 +129,9 @@
<LinearLayout
android:id="@+id/app_ops"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:layout_marginStart="6dp"
android:orientation="horizontal" >
<ImageButton
android:id="@+id/camera"
@@ -139,7 +140,6 @@
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

View File

@@ -70,6 +70,11 @@ public interface NotificationMenuRowPlugin extends Plugin {
*/
public MenuItem getLongpressMenuItem(Context context);
/**
* @return the {@link MenuItem} to display when app ops icons are pressed.
*/
public MenuItem getAppOpsMenuItem(Context context);
public void setMenuItems(ArrayList<MenuItem> items);
public void setMenuClickListener(OnMenuEventListener listener);

View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 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.
-->
<com.android.systemui.statusbar.AppOpsInfo
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/app_ops_info"
android:clickable="true"
android:orientation="vertical"
android:paddingStart="@*android:dimen/notification_content_margin_start"
android:paddingEnd="@*android:dimen/notification_content_margin_end"
android:background="@color/notification_guts_bg_color"
android:theme="@*android:style/Theme.DeviceDefault.Light">
<!-- Package Info -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:layout_marginTop="@*android:dimen/notification_header_padding_top" >
<ImageView
android:id="@+id/pkgicon"
android:layout_width="@dimen/notification_guts_header_height"
android:layout_height="@dimen/notification_guts_header_height"
android:layout_centerVertical="true"
android:layout_marginEnd="3dp" />
<TextView
android:id="@+id/pkgname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@*android:style/TextAppearance.Material.Notification.Info"
android:layout_marginStart="3dp"
android:layout_marginEnd="2dp"
android:singleLine="true"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/pkgicon" />
</RelativeLayout>
<TextView
android:id="@+id/prompt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@*android:dimen/notification_header_padding_top"
style="@style/TextAppearance.NotificationInfo.Secondary" />
<!-- Settings and Done buttons -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="@dimen/notification_guts_button_spacing"
android:layout_marginBottom="@dimen/notification_guts_button_spacing"
android:gravity="end" >
<TextView
android:id="@+id/settings"
android:text="@string/notification_appops_settings"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="@drawable/ripple_drawable"
style="@style/TextAppearance.NotificationInfo.Button"/>
<TextView
android:id="@+id/ok"
android:text="@string/notification_appops_ok"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="@drawable/ripple_drawable"
android:minWidth="48dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="-8dp"
style="@style/TextAppearance.NotificationInfo.Button"/>
</LinearLayout>
</com.android.systemui.statusbar.AppOpsInfo>

View File

@@ -1545,6 +1545,25 @@
<!-- Notification: Control panel: Label that displays when the app's notifications cannot be blocked. -->
<string name="notification_unblockable_desc">These notifications can\'t be turned off</string>
<string name="notification_appops_camera_active">camera</string>
<string name="notification_appops_microphone_active">microphone</string>
<string name="notification_appops_overlay_active">displaying over other apps on your screen</string>
<plurals name="notification_appops">
<item quantity="one">This app is <xliff:g id="performing activity" example="using the camera">%1$s</xliff:g>.</item>
<item quantity="other">This app is <xliff:g id="performing activity" example="using the camera">%1$s</xliff:g> and <xliff:g id="performing activity" example="using the microphone">%2$s</xliff:g>.</item>
</plurals>
<plurals name="notification_using">
<item quantity="one">using the <xliff:g id="performing activity" example="camera">%1$s</xliff:g></item>
<item quantity="other">using the <xliff:g id="performing activity" example="camera">%1$s</xliff:g> and <xliff:g id="performing activity" example="microphone">%2$s</xliff:g></item>
</plurals>
<string name="notification_appops_settings">Settings</string>
<string name="notification_appops_ok">Ok</string>
<!-- Notification: Control panel: Accessibility description for expanded inline controls view, used
to control settings about notifications related to the current notification. -->
<string name="notification_channel_controls_opened_accessibility">Notification controls for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g> opened</string>

View File

@@ -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<Integer> 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<Integer> ops);
}
public void bindGuts(final PackageManager pm,
final OnSettingsClickListener onSettingsClick,
final StatusBarNotification sbn,
ArraySet<Integer> 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();
}
}

View File

@@ -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<MenuItem> 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);
}
}

View File

@@ -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());

View File

@@ -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<Integer> 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<Integer> 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,

View File

@@ -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<MenuItem> 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;
}

View File

@@ -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();

View File

@@ -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<Integer> expectedOps = new ArraySet<>();
expectedOps.add(OP_CAMERA);
final CountDownLatch latch = new CountDownLatch(1);
mAppOpsInfo.bindGuts(mMockPackageManager, (View v, String pkg, int uid,
ArraySet<Integer> ops) -> {
assertEquals(TEST_PACKAGE_NAME, pkg);
assertEquals(expectedOps, ops);
assertEquals(TEST_UID, uid);
latch.countDown();
}, mSbn, 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<Integer> expectedOps = new ArraySet<>();
expectedOps.add(OP_CAMERA);
final CountDownLatch latch = new CountDownLatch(1);
mAppOpsInfo.bindGuts(mMockPackageManager, (View v, String pkg, int uid,
ArraySet<Integer> ops) -> {
assertEquals(TEST_PACKAGE_NAME, pkg);
assertEquals(expectedOps, ops);
assertEquals(TEST_UID, uid);
latch.countDown();
}, mSbn, 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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());
}
}

View File

@@ -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());
}
}

View File

@@ -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;

View File

@@ -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<Integer> ops = new ArraySet<>();
ops.add(OP_CAMERA);
mGutsManager.startAppOpsSettingsActivity("", 0, ops, null);
ArgumentCaptor<Intent> 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<Integer> ops = new ArraySet<>();
ops.add(OP_RECORD_AUDIO);
mGutsManager.startAppOpsSettingsActivity("", 0, ops, null);
ArgumentCaptor<Intent> 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<Integer> ops = new ArraySet<>();
ops.add(OP_CAMERA);
ops.add(OP_RECORD_AUDIO);
mGutsManager.startAppOpsSettingsActivity("", 0, ops, null);
ArgumentCaptor<Intent> 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<Integer> ops = new ArraySet<>();
ops.add(OP_SYSTEM_ALERT_WINDOW);
mGutsManager.startAppOpsSettingsActivity("", 0, ops, null);
ArgumentCaptor<Intent> 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<Integer> ops = new ArraySet<>();
ops.add(OP_CAMERA);
ops.add(OP_RECORD_AUDIO);
ops.add(OP_SYSTEM_ALERT_WINDOW);
mGutsManager.startAppOpsSettingsActivity("", 0, ops, null);
ArgumentCaptor<Intent> 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<Integer> ops = new ArraySet<>();
ops.add(OP_CAMERA);
ops.add(OP_SYSTEM_ALERT_WINDOW);
mGutsManager.startAppOpsSettingsActivity("", 0, ops, null);
ArgumentCaptor<Intent> 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<Integer> ops = new ArraySet<>();
ops.add(OP_RECORD_AUDIO);
ops.add(OP_SYSTEM_ALERT_WINDOW);
mGutsManager.startAppOpsSettingsActivity("", 0, ops, null);
ArgumentCaptor<Intent> 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:

View File

@@ -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());
}
}