Merge "Show app ops guts when tap app ops icons" into pi-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
7b894b3fcd
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
89
packages/SystemUI/res/layout/app_ops_info.xml
Normal file
89
packages/SystemUI/res/layout/app_ops_info.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user