diff --git a/core/java/android/app/PendingIntent.java b/core/java/android/app/PendingIntent.java index 315259bdf3887..bdaf80e374dfe 100644 --- a/core/java/android/app/PendingIntent.java +++ b/core/java/android/app/PendingIntent.java @@ -33,8 +33,11 @@ import android.os.Parcelable; import android.os.RemoteException; import android.os.UserHandle; import android.util.AndroidException; +import android.util.ArraySet; import android.util.proto.ProtoOutputStream; +import com.android.internal.os.IResultReceiver; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -93,7 +96,9 @@ import java.lang.annotation.RetentionPolicy; */ public final class PendingIntent implements Parcelable { private final IIntentSender mTarget; + private IResultReceiver mCancelReceiver; private IBinder mWhitelistToken; + private ArraySet mCancelListeners; /** @hide */ @IntDef(flag = true, @@ -963,6 +968,74 @@ public final class PendingIntent implements Parcelable { } } + /** + * Register a listener to when this pendingIntent is cancelled. There are no guarantees on which + * thread a listener will be called and it's up to the caller to synchronize. This may + * trigger a synchronous binder call so should therefore usually be called on a background + * thread. + * + * @hide + */ + public void registerCancelListener(CancelListener cancelListener) { + synchronized (this) { + if (mCancelReceiver == null) { + mCancelReceiver = new IResultReceiver.Stub() { + @Override + public void send(int resultCode, Bundle resultData) throws RemoteException { + notifyCancelListeners(); + } + }; + } + if (mCancelListeners == null) { + mCancelListeners = new ArraySet<>(); + } + boolean wasEmpty = mCancelListeners.isEmpty(); + mCancelListeners.add(cancelListener); + if (wasEmpty) { + try { + ActivityManager.getService().registerIntentSenderCancelListener(mTarget, + mCancelReceiver); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + } + + private void notifyCancelListeners() { + ArraySet cancelListeners; + synchronized (this) { + cancelListeners = new ArraySet<>(mCancelListeners); + } + int size = cancelListeners.size(); + for (int i = 0; i < size; i++) { + cancelListeners.valueAt(i).onCancelled(this); + } + } + + /** + * Un-register a listener to when this pendingIntent is cancelled. + * + * @hide + */ + public void unregisterCancelListener(CancelListener cancelListener) { + synchronized (this) { + if (mCancelListeners == null) { + return; + } + boolean wasEmpty = mCancelListeners.isEmpty(); + mCancelListeners.remove(cancelListener); + if (mCancelListeners.isEmpty() && !wasEmpty) { + try { + ActivityManager.getService().unregisterIntentSenderCancelListener(mTarget, + mCancelReceiver); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + } + /** * Return the user handle of the application that created this * PendingIntent, that is the user under which you will actually be @@ -1184,4 +1257,18 @@ public final class PendingIntent implements Parcelable { public IBinder getWhitelistToken() { return mWhitelistToken; } + + /** + * A listener to when a pending intent is cancelled + * + * @hide + */ + public interface CancelListener { + /** + * Called when a Pending Intent is cancelled. + * + * @param intent The intent that was cancelled. + */ + void onCancelled(PendingIntent intent); + } } diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index ff9daee180bff..5ecbf90a4ecac 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -946,6 +946,7 @@ public class RemoteViews implements Parcelable, Filter { } }; } + target.setTagInternal(R.id.pending_intent_tag, pendingIntent); target.setOnClickListener(listener); } diff --git a/core/res/res/values/ids.xml b/core/res/res/values/ids.xml index c90a0df5d7b1d..47d04edfc281c 100644 --- a/core/res/res/values/ids.xml +++ b/core/res/res/values/ids.xml @@ -128,6 +128,7 @@ + diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 3ee333cadadce..e0dfa00f7dbfa 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2615,6 +2615,7 @@ + diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java index 5f7b638dc872b..9ed5b7fd45743 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java @@ -16,16 +16,21 @@ package com.android.systemui.statusbar.notification; +import android.app.PendingIntent; import android.content.Context; import android.graphics.Color; import android.graphics.Rect; import android.service.notification.StatusBarNotification; +import android.util.ArraySet; import android.view.View; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; +import com.android.internal.R; import com.android.internal.widget.NotificationActionListLayout; +import com.android.systemui.Dependency; +import com.android.systemui.UiOffloadThread; import com.android.systemui.statusbar.CrossFadeHelper; import com.android.systemui.statusbar.ExpandableNotificationRow; import com.android.systemui.statusbar.TransformableView; @@ -49,6 +54,8 @@ public class NotificationTemplateViewWrapper extends NotificationHeaderViewWrapp private int mContentHeight; private int mMinHeightHint; private NotificationActionListLayout mActions; + private ArraySet mCancelledPendingIntents = new ArraySet<>(); + private UiOffloadThread mUiOffloadThread; protected NotificationTemplateViewWrapper(Context ctx, View view, ExpandableNotificationRow row) { @@ -137,6 +144,63 @@ public class NotificationTemplateViewWrapper extends NotificationHeaderViewWrapp mActionsContainer = mView.findViewById(com.android.internal.R.id.actions_container); mActions = mView.findViewById(com.android.internal.R.id.actions); mReplyAction = mView.findViewById(com.android.internal.R.id.reply_icon_action); + updatePendingIntentCancellations(); + } + + private void updatePendingIntentCancellations() { + if (mActions != null) { + int numActions = mActions.getChildCount(); + for (int i = 0; i < numActions; i++) { + View action = mActions.getChildAt(i); + performOnPendingIntentCancellation(action, () -> { + action.setEnabled(false); + // The visual appearance doesn't look disabled enough yet, let's add the + // alpha as well. Selectors unfortunately don't seem to work here. + action.setAlpha(0.5f); + }); + } + } + if (mReplyAction != null) { + performOnPendingIntentCancellation(mReplyAction, () -> { + mReplyAction.setEnabled(false); + // The visual appearance doesn't look disabled enough yet, let's add the + // alpha as well. Selectors unfortunately don't seem to work here. + mReplyAction.setAlpha(0.5f); + }); + } + } + + private void performOnPendingIntentCancellation(View view, Runnable cancellationRunnable) { + PendingIntent pendingIntent = (PendingIntent) view.getTag( + com.android.internal.R.id.pending_intent_tag); + if (pendingIntent == null) { + return; + } + if (mCancelledPendingIntents.contains(pendingIntent)) { + cancellationRunnable.run(); + } else { + PendingIntent.CancelListener listener = (PendingIntent intent) -> { + mView.post(() -> { + mCancelledPendingIntents.add(pendingIntent); + cancellationRunnable.run(); + }); + }; + if (mUiOffloadThread == null) { + mUiOffloadThread = Dependency.get(UiOffloadThread.class); + } + mUiOffloadThread.submit(() -> pendingIntent.registerCancelListener(listener)); + view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + mUiOffloadThread.submit(() -> pendingIntent.registerCancelListener(listener)); + } + + @Override + public void onViewDetachedFromWindow(View v) { + mUiOffloadThread.submit(() -> pendingIntent.unregisterCancelListener(listener)); + } + }); + } } @Override diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 62a055d330c81..22adf352ac609 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -8476,8 +8476,19 @@ public class ActivityManagerService extends IActivityManager.Stub if (!(sender instanceof PendingIntentRecord)) { return; } + boolean isCancelled; synchronized(this) { - ((PendingIntentRecord)sender).registerCancelListenerLocked(receiver); + PendingIntentRecord pendingIntent = (PendingIntentRecord) sender; + isCancelled = pendingIntent.canceled; + if (!isCancelled) { + pendingIntent.registerCancelListenerLocked(receiver); + } + } + if (isCancelled) { + try { + receiver.send(Activity.RESULT_CANCELED, null); + } catch (RemoteException e) { + } } }