Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2a8d8bb7 authored by Julia Reynolds's avatar Julia Reynolds
Browse files

Allow apps to require auth before triggering actions

Test: atest; cts in topic
Bug: 162100703
Change-Id: I25354858104eb7fbb343dbb006cd8bf9e48ae3e1
parent 84e70df9
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -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 −8
Original line number Diff line number Diff line
@@ -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 =
+2 −0
Original line number Diff line number Diff line
@@ -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);
  }
+21 −6
Original line number Diff line number Diff line
@@ -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
+35 −23
Original line number Diff line number Diff line
@@ -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,47 +172,56 @@ public class NotificationRemoteInputManager implements Dumpable {
            });
        }

        private void logActionClick(
                View view,
                NotificationEntry entry,
                PendingIntent actionIntent) {
        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) {
                // Custom action button, not logging.
                return;
                return null;
            }
            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();
            int buttonIndex = -1;
            // If this is a default template, determine the index of the button.
            if (view.getId() == com.android.internal.R.id.action0 &&
                    parent != null && parent instanceof ViewGroup) {
                ViewGroup actionGroup = (ViewGroup) parent;
                buttonIndex = actionGroup.indexOfChild(view);
                return null;
            }
            final int count = mEntryManager.getActiveNotificationsCount();
            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.
            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;
                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) {
            Notification.Action action = getActionFromView(view, entry, actionIntent);
            if (action == null) {
                return;
            }
            ViewParent parent = view.getParent();
            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 &&
                    parent != null && parent instanceof ViewGroup) {
                ViewGroup actionGroup = (ViewGroup) parent;
                buttonIndex = actionGroup.indexOfChild(view);
            }
            final int count = mEntryManager.getActiveNotificationsCount();
            final int rank = mEntryManager
                    .getActiveNotificationUnfiltered(key).getRanking().getRank();

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

    /**
Loading