Allow apps to require auth before triggering actions
Test: atest; cts in topic Bug: 162100703 Change-Id: I25354858104eb7fbb343dbb006cd8bf9e48ae3e1
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user