Allow apps to require auth before triggering actions

Test: atest; cts in topic
Bug: 162100703
Change-Id: I25354858104eb7fbb343dbb006cd8bf9e48ae3e1
This commit is contained in:
Julia Reynolds
2020-08-10 14:05:26 -04:00
parent 84e70df9d4
commit 2a8d8bb7dc
7 changed files with 112 additions and 42 deletions

View File

@@ -5596,6 +5596,7 @@ package android.app {
method public android.graphics.drawable.Icon getIcon();
method public android.app.RemoteInput[] getRemoteInputs();
method public int getSemanticAction();
method public boolean isAuthenticationRequired();
method public boolean isContextual();
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.app.Notification.Action> CREATOR;
@@ -5625,6 +5626,7 @@ package android.app {
method @NonNull public android.app.Notification.Action.Builder extend(android.app.Notification.Action.Extender);
method @NonNull public android.os.Bundle getExtras();
method @NonNull public android.app.Notification.Action.Builder setAllowGeneratedReplies(boolean);
method @NonNull public android.app.Notification.Action.Builder setAuthenticationRequired(boolean);
method @NonNull public android.app.Notification.Action.Builder setContextual(boolean);
method @NonNull public android.app.Notification.Action.Builder setSemanticAction(int);
}

View File

@@ -1492,6 +1492,7 @@ public class Notification implements Parcelable
private boolean mAllowGeneratedReplies = true;
private final @SemanticAction int mSemanticAction;
private final boolean mIsContextual;
private boolean mAuthenticationRequired;
/**
* Small icon representing the action.
@@ -1528,6 +1529,7 @@ public class Notification implements Parcelable
mAllowGeneratedReplies = in.readInt() == 1;
mSemanticAction = in.readInt();
mIsContextual = in.readInt() == 1;
mAuthenticationRequired = in.readInt() == 1;
}
/**
@@ -1536,13 +1538,14 @@ public class Notification implements Parcelable
@Deprecated
public Action(int icon, CharSequence title, PendingIntent intent) {
this(Icon.createWithResource("", icon), title, intent, new Bundle(), null, true,
SEMANTIC_ACTION_NONE, false /* isContextual */);
SEMANTIC_ACTION_NONE, false /* isContextual */, false /* requireAuth */);
}
/** Keep in sync with {@link Notification.Action.Builder#Builder(Action)}! */
private Action(Icon icon, CharSequence title, PendingIntent intent, Bundle extras,
RemoteInput[] remoteInputs, boolean allowGeneratedReplies,
@SemanticAction int semanticAction, boolean isContextual) {
@SemanticAction int semanticAction, boolean isContextual,
boolean requireAuth) {
this.mIcon = icon;
if (icon != null && icon.getType() == Icon.TYPE_RESOURCE) {
this.icon = icon.getResId();
@@ -1554,6 +1557,7 @@ public class Notification implements Parcelable
this.mAllowGeneratedReplies = allowGeneratedReplies;
this.mSemanticAction = semanticAction;
this.mIsContextual = isContextual;
this.mAuthenticationRequired = requireAuth;
}
/**
@@ -1623,6 +1627,17 @@ public class Notification implements Parcelable
return getParcelableArrayFromBundle(mExtras, EXTRA_DATA_ONLY_INPUTS, RemoteInput.class);
}
/**
* Returns whether the OS should only send this action's {@link PendingIntent} on an
* unlocked device.
*
* If the device is locked when the action is invoked, the OS should show the keyguard and
* require successful authentication before invoking the intent.
*/
public boolean isAuthenticationRequired() {
return mAuthenticationRequired;
}
/**
* Builder class for {@link Action} objects.
*/
@@ -1635,6 +1650,7 @@ public class Notification implements Parcelable
@Nullable private ArrayList<RemoteInput> mRemoteInputs;
private @SemanticAction int mSemanticAction;
private boolean mIsContextual;
private boolean mAuthenticationRequired;
/**
* Construct a new builder for {@link Action} object.
@@ -1654,7 +1670,7 @@ public class Notification implements Parcelable
* @param intent the {@link PendingIntent} to fire when users trigger this action
*/
public Builder(Icon icon, CharSequence title, PendingIntent intent) {
this(icon, title, intent, new Bundle(), null, true, SEMANTIC_ACTION_NONE);
this(icon, title, intent, new Bundle(), null, true, SEMANTIC_ACTION_NONE, false);
}
/**
@@ -1665,23 +1681,25 @@ public class Notification implements Parcelable
public Builder(Action action) {
this(action.getIcon(), action.title, action.actionIntent,
new Bundle(action.mExtras), action.getRemoteInputs(),
action.getAllowGeneratedReplies(), action.getSemanticAction());
action.getAllowGeneratedReplies(), action.getSemanticAction(),
action.isAuthenticationRequired());
}
private Builder(@Nullable Icon icon, @Nullable CharSequence title,
@Nullable PendingIntent intent, @NonNull Bundle extras,
@Nullable RemoteInput[] remoteInputs, boolean allowGeneratedReplies,
@SemanticAction int semanticAction) {
@SemanticAction int semanticAction, boolean authRequired) {
mIcon = icon;
mTitle = title;
mIntent = intent;
mExtras = extras;
if (remoteInputs != null) {
mRemoteInputs = new ArrayList<RemoteInput>(remoteInputs.length);
mRemoteInputs = new ArrayList<>(remoteInputs.length);
Collections.addAll(mRemoteInputs, remoteInputs);
}
mAllowGeneratedReplies = allowGeneratedReplies;
mSemanticAction = semanticAction;
mAuthenticationRequired = authRequired;
}
/**
@@ -1775,6 +1793,21 @@ public class Notification implements Parcelable
return this;
}
/**
* Sets whether the OS should only send this action's {@link PendingIntent} on an
* unlocked device.
*
* If this is true and the device is locked when the action is invoked, the OS will
* show the keyguard and require successful authentication before invoking the intent.
* If this is false and the device is locked, the OS will decide whether authentication
* should be required.
*/
@NonNull
public Builder setAuthenticationRequired(boolean authenticationRequired) {
mAuthenticationRequired = authenticationRequired;
return this;
}
/**
* Throws an NPE if we are building a contextual action missing one of the fields
* necessary to display the action.
@@ -1827,7 +1860,8 @@ public class Notification implements Parcelable
RemoteInput[] textInputsArr = textInputs.isEmpty()
? null : textInputs.toArray(new RemoteInput[textInputs.size()]);
return new Action(mIcon, mTitle, mIntent, mExtras, textInputsArr,
mAllowGeneratedReplies, mSemanticAction, mIsContextual);
mAllowGeneratedReplies, mSemanticAction, mIsContextual,
mAuthenticationRequired);
}
}
@@ -1841,7 +1875,8 @@ public class Notification implements Parcelable
getRemoteInputs(),
getAllowGeneratedReplies(),
getSemanticAction(),
isContextual());
isContextual(),
isAuthenticationRequired());
}
@Override
@@ -1870,6 +1905,7 @@ public class Notification implements Parcelable
out.writeInt(mAllowGeneratedReplies ? 1 : 0);
out.writeInt(mSemanticAction);
out.writeInt(mIsContextual ? 1 : 0);
out.writeInt(mAuthenticationRequired ? 1 : 0);
}
public static final @android.annotation.NonNull Parcelable.Creator<Action> CREATOR =

View File

@@ -5596,6 +5596,7 @@ package android.app {
method public android.graphics.drawable.Icon getIcon();
method public android.app.RemoteInput[] getRemoteInputs();
method public int getSemanticAction();
method public boolean isAuthenticationRequired();
method public boolean isContextual();
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.app.Notification.Action> CREATOR;
@@ -5625,6 +5626,7 @@ package android.app {
method @NonNull public android.app.Notification.Action.Builder extend(android.app.Notification.Action.Extender);
method @NonNull public android.os.Bundle getExtras();
method @NonNull public android.app.Notification.Action.Builder setAllowGeneratedReplies(boolean);
method @NonNull public android.app.Notification.Action.Builder setAuthenticationRequired(boolean);
method @NonNull public android.app.Notification.Action.Builder setContextual(boolean);
method @NonNull public android.app.Notification.Action.Builder setSemanticAction(int);
}

View File

@@ -44,6 +44,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.notification.MediaNotificationProcessor
import com.android.systemui.statusbar.notification.row.HybridGroupManager
import com.android.systemui.util.Assert
@@ -96,6 +97,7 @@ class MediaDataManager(
dumpManager: DumpManager,
mediaTimeoutListener: MediaTimeoutListener,
mediaResumeListener: MediaResumeListener,
private val activityStarter: ActivityStarter,
private var useMediaResumption: Boolean,
private val useQsMediaPlayer: Boolean
) : Dumpable {
@@ -112,10 +114,11 @@ class MediaDataManager(
dumpManager: DumpManager,
broadcastDispatcher: BroadcastDispatcher,
mediaTimeoutListener: MediaTimeoutListener,
mediaResumeListener: MediaResumeListener
mediaResumeListener: MediaResumeListener,
activityStarter: ActivityStarter
) : this(context, backgroundExecutor, foregroundExecutor, mediaControllerFactory,
broadcastDispatcher, dumpManager, mediaTimeoutListener, mediaResumeListener,
Utils.useMediaResumption(context), Utils.useQsMediaPlayer(context))
activityStarter, Utils.useMediaResumption(context), Utils.useQsMediaPlayer(context))
private val appChangeReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
@@ -393,10 +396,13 @@ class MediaDataManager(
}
val runnable = if (action.actionIntent != null) {
Runnable {
try {
action.actionIntent.send()
} catch (e: PendingIntent.CanceledException) {
Log.d(TAG, "Intent canceled", e)
if (action.isAuthenticationRequired()) {
activityStarter.dismissKeyguardThenExecute ({
var result = sendPendingIntent(action.actionIntent)
result
}, {}, true)
} else {
sendPendingIntent(action.actionIntent)
}
}
} else {
@@ -439,6 +445,15 @@ class MediaDataManager(
return null
}
private fun sendPendingIntent(intent: PendingIntent): Boolean {
return try {
intent.send()
true
} catch (e: PendingIntent.CanceledException) {
Log.d(TAG, "Intent canceled", e)
false
}
}
/**
* Load a bitmap from a URI
* @param uri the uri to load

View File

@@ -161,7 +161,9 @@ public class NotificationRemoteInputManager implements Dumpable {
ActivityManager.getService().resumeAppSwitches();
} catch (RemoteException e) {
}
return mCallback.handleRemoteViewClick(view, pendingIntent, () -> {
Notification.Action action = getActionFromView(view, entry, pendingIntent);
return mCallback.handleRemoteViewClick(view, pendingIntent,
action == null ? false : action.isAuthenticationRequired(), () -> {
Pair<Intent, ActivityOptions> options = response.getLaunchOptions(view);
options.second.setLaunchWindowingMode(
WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY);
@@ -170,23 +172,45 @@ public class NotificationRemoteInputManager implements Dumpable {
});
}
private @Nullable Notification.Action getActionFromView(View view,
NotificationEntry entry, PendingIntent actionIntent) {
Integer actionIndex = (Integer)
view.getTag(com.android.internal.R.id.notification_action_index_tag);
if (actionIndex == null) {
return null;
}
if (entry == null) {
Log.w(TAG, "Couldn't determine notification for click.");
return null;
}
// Notification may be updated before this function is executed, and thus play safe
// here and verify that the action object is still the one that where the click happens.
StatusBarNotification statusBarNotification = entry.getSbn();
Notification.Action[] actions = statusBarNotification.getNotification().actions;
if (actions == null || actionIndex >= actions.length) {
Log.w(TAG, "statusBarNotification.getNotification().actions is null or invalid");
return null ;
}
final Notification.Action action =
statusBarNotification.getNotification().actions[actionIndex];
if (!Objects.equals(action.actionIntent, actionIntent)) {
Log.w(TAG, "actionIntent does not match");
return null;
}
return action;
}
private void logActionClick(
View view,
NotificationEntry entry,
PendingIntent actionIntent) {
Integer actionIndex = (Integer)
view.getTag(com.android.internal.R.id.notification_action_index_tag);
if (actionIndex == null) {
// Custom action button, not logging.
Notification.Action action = getActionFromView(view, entry, actionIntent);
if (action == null) {
return;
}
ViewParent parent = view.getParent();
if (entry == null) {
Log.w(TAG, "Couldn't determine notification for click.");
return;
}
StatusBarNotification statusBarNotification = entry.getSbn();
String key = statusBarNotification.getKey();
String key = entry.getSbn().getKey();
int buttonIndex = -1;
// If this is a default template, determine the index of the button.
if (view.getId() == com.android.internal.R.id.action0 &&
@@ -198,19 +222,6 @@ public class NotificationRemoteInputManager implements Dumpable {
final int rank = mEntryManager
.getActiveNotificationUnfiltered(key).getRanking().getRank();
// Notification may be updated before this function is executed, and thus play safe
// here and verify that the action object is still the one that where the click happens.
Notification.Action[] actions = statusBarNotification.getNotification().actions;
if (actions == null || actionIndex >= actions.length) {
Log.w(TAG, "statusBarNotification.getNotification().actions is null or invalid");
return;
}
final Notification.Action action =
statusBarNotification.getNotification().actions[actionIndex];
if (!Objects.equals(action.actionIntent, actionIntent)) {
Log.w(TAG, "actionIntent does not match");
return;
}
NotificationVisibility.NotificationLocation location =
NotificationLogger.getNotificationLocation(
mEntryManager.getActiveNotificationUnfiltered(key));
@@ -813,11 +824,12 @@ public class NotificationRemoteInputManager implements Dumpable {
*
* @param view
* @param pendingIntent
* @param appRequestedAuth
* @param defaultHandler
* @return true iff the click was handled
*/
boolean handleRemoteViewClick(View view, PendingIntent pendingIntent,
ClickHandler defaultHandler);
boolean appRequestedAuth, ClickHandler defaultHandler);
}
/**

View File

@@ -244,9 +244,10 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks,
@Override
public boolean handleRemoteViewClick(View view, PendingIntent pendingIntent,
boolean appRequestedAuth,
NotificationRemoteInputManager.ClickHandler defaultHandler) {
final boolean isActivity = pendingIntent.isActivity();
if (isActivity) {
if (isActivity || appRequestedAuth) {
mActionClickLogger.logWaitingToCloseKeyguard(pendingIntent);
final boolean afterKeyguardGone = mActivityIntentHelper.wouldLaunchResolverActivity(
pendingIntent.getIntent(), mLockscreenUserManager.getCurrentUserId());

View File

@@ -13,6 +13,7 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.SbnBuilder
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.eq
@@ -58,6 +59,7 @@ class MediaDataManagerTest : SysuiTestCase() {
@Mock lateinit var mediaTimeoutListener: MediaTimeoutListener
@Mock lateinit var mediaResumeListener: MediaResumeListener
@Mock lateinit var pendingIntent: PendingIntent
@Mock lateinit var activityStarter: ActivityStarter
@JvmField @Rule val mockito = MockitoJUnit.rule()
lateinit var mediaDataManager: MediaDataManager
lateinit var mediaNotification: StatusBarNotification
@@ -68,8 +70,8 @@ class MediaDataManagerTest : SysuiTestCase() {
backgroundExecutor = FakeExecutor(FakeSystemClock())
mediaDataManager = MediaDataManager(context, backgroundExecutor, foregroundExecutor,
mediaControllerFactory, broadcastDispatcher, dumpManager,
mediaTimeoutListener, mediaResumeListener, useMediaResumption = true,
useQsMediaPlayer = true)
mediaTimeoutListener, mediaResumeListener, activityStarter,
useMediaResumption = true, useQsMediaPlayer = true)
session = MediaSession(context, "MediaDataManagerTestSession")
mediaNotification = SbnBuilder().run {
setPkg(PACKAGE_NAME)