| /* |
| * Copyright (C) 2017 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.WindowConfiguration.WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY; |
| |
| import android.annotation.NonNull; |
| import android.app.ActivityManager; |
| import android.app.ActivityOptions; |
| import android.app.KeyguardManager; |
| import android.app.Notification; |
| import android.app.PendingIntent; |
| import android.app.RemoteInput; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.os.RemoteException; |
| import android.os.ServiceManager; |
| import android.os.SystemClock; |
| import android.os.SystemProperties; |
| import android.os.UserManager; |
| import android.service.notification.StatusBarNotification; |
| import android.text.TextUtils; |
| import android.util.ArraySet; |
| import android.util.Log; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.ViewParent; |
| import android.widget.RemoteViews; |
| import android.widget.TextView; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.statusbar.IStatusBarService; |
| import com.android.internal.statusbar.NotificationVisibility; |
| import com.android.systemui.Dependency; |
| import com.android.systemui.Dumpable; |
| import com.android.systemui.InitController; |
| import com.android.systemui.statusbar.notification.NotificationData; |
| import com.android.systemui.statusbar.notification.NotificationEntryManager; |
| import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; |
| import com.android.systemui.statusbar.phone.ShadeController; |
| import com.android.systemui.statusbar.policy.RemoteInputView; |
| |
| import java.io.FileDescriptor; |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.Set; |
| |
| /** |
| * Class for handling remote input state over a set of notifications. This class handles things |
| * like keeping notifications temporarily that were cancelled as a response to a remote input |
| * interaction, keeping track of notifications to remove when NotificationPresenter is collapsed, |
| * and handling clicks on remote views. |
| */ |
| public class NotificationRemoteInputManager implements Dumpable { |
| public static final boolean ENABLE_REMOTE_INPUT = |
| SystemProperties.getBoolean("debug.enable_remote_input", true); |
| public static boolean FORCE_REMOTE_INPUT_HISTORY = |
| SystemProperties.getBoolean("debug.force_remoteinput_history", true); |
| private static final boolean DEBUG = false; |
| private static final String TAG = "NotifRemoteInputManager"; |
| |
| /** |
| * How long to wait before auto-dismissing a notification that was kept for remote input, and |
| * has now sent a remote input. We auto-dismiss, because the app may not see a reason to cancel |
| * these given that they technically don't exist anymore. We wait a bit in case the app issues |
| * an update. |
| */ |
| private static final int REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY = 200; |
| |
| /** |
| * Notifications that are already removed but are kept around because we want to show the |
| * remote input history. See {@link RemoteInputHistoryExtender} and |
| * {@link SmartReplyHistoryExtender}. |
| */ |
| protected final ArraySet<String> mKeysKeptForRemoteInputHistory = new ArraySet<>(); |
| |
| /** |
| * Notifications that are already removed but are kept around because the remote input is |
| * actively being used (i.e. user is typing in it). See {@link RemoteInputActiveExtender}. |
| */ |
| protected final ArraySet<NotificationData.Entry> mEntriesKeptForRemoteInputActive = |
| new ArraySet<>(); |
| |
| // Dependencies: |
| protected final NotificationLockscreenUserManager mLockscreenUserManager = |
| Dependency.get(NotificationLockscreenUserManager.class); |
| protected final SmartReplyController mSmartReplyController = |
| Dependency.get(SmartReplyController.class); |
| private final NotificationEntryManager mEntryManager |
| = Dependency.get(NotificationEntryManager.class); |
| |
| // Lazy |
| private ShadeController mShadeController; |
| |
| protected final Context mContext; |
| private final UserManager mUserManager; |
| private final KeyguardManager mKeyguardManager; |
| |
| protected RemoteInputController mRemoteInputController; |
| protected NotificationPresenter mPresenter; |
| protected NotificationLifetimeExtender.NotificationSafeToRemoveCallback |
| mNotificationLifetimeFinishedCallback; |
| protected IStatusBarService mBarService; |
| protected Callback mCallback; |
| protected final ArrayList<NotificationLifetimeExtender> mLifetimeExtenders = new ArrayList<>(); |
| |
| private final RemoteViews.OnClickHandler mOnClickHandler = new RemoteViews.OnClickHandler() { |
| |
| @Override |
| public boolean onClickHandler( |
| final View view, final PendingIntent pendingIntent, final Intent fillInIntent) { |
| getShadeController().wakeUpIfDozing(SystemClock.uptimeMillis(), view); |
| |
| if (handleRemoteInput(view, pendingIntent)) { |
| return true; |
| } |
| |
| if (DEBUG) { |
| Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent); |
| } |
| logActionClick(view); |
| // The intent we are sending is for the application, which |
| // won't have permission to immediately start an activity after |
| // the user switches to home. We know it is safe to do at this |
| // point, so make sure new activity switches are now allowed. |
| try { |
| ActivityManager.getService().resumeAppSwitches(); |
| } catch (RemoteException e) { |
| } |
| return mCallback.handleRemoteViewClick(view, pendingIntent, fillInIntent, |
| () -> super.onClickHandler(view, pendingIntent, fillInIntent)); |
| } |
| |
| private void logActionClick(View view) { |
| ViewParent parent = view.getParent(); |
| String key = getNotificationKeyForParent(parent); |
| if (key == null) { |
| Log.w(TAG, "Couldn't determine notification for click."); |
| return; |
| } |
| int index = -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; |
| index = actionGroup.indexOfChild(view); |
| } |
| final int count = mEntryManager.getNotificationData().getActiveNotifications().size(); |
| final int rank = mEntryManager.getNotificationData().getRank(key); |
| final NotificationVisibility nv = NotificationVisibility.obtain(key, rank, count, true); |
| try { |
| mBarService.onNotificationActionClick(key, index, nv); |
| } catch (RemoteException e) { |
| // Ignore |
| } |
| } |
| |
| private String getNotificationKeyForParent(ViewParent parent) { |
| while (parent != null) { |
| if (parent instanceof ExpandableNotificationRow) { |
| return ((ExpandableNotificationRow) parent) |
| .getStatusBarNotification().getKey(); |
| } |
| parent = parent.getParent(); |
| } |
| return null; |
| } |
| |
| @Override |
| protected ActivityOptions getActivityOptions(Context context) { |
| ActivityOptions options = super.getActivityOptions(context); |
| options.setLaunchWindowingMode(WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY); |
| return options; |
| } |
| |
| private boolean handleRemoteInput(View view, PendingIntent pendingIntent) { |
| if (mCallback.shouldHandleRemoteInput(view, pendingIntent)) { |
| return true; |
| } |
| |
| Object tag = view.getTag(com.android.internal.R.id.remote_input_tag); |
| RemoteInput[] inputs = null; |
| if (tag instanceof RemoteInput[]) { |
| inputs = (RemoteInput[]) tag; |
| } |
| |
| if (inputs == null) { |
| return false; |
| } |
| |
| RemoteInput input = null; |
| |
| for (RemoteInput i : inputs) { |
| if (i.getAllowFreeFormInput()) { |
| input = i; |
| } |
| } |
| |
| if (input == null) { |
| return false; |
| } |
| |
| ViewParent p = view.getParent(); |
| RemoteInputView riv = null; |
| while (p != null) { |
| if (p instanceof View) { |
| View pv = (View) p; |
| if (pv.isRootNamespace()) { |
| riv = findRemoteInputView(pv); |
| break; |
| } |
| } |
| p = p.getParent(); |
| } |
| ExpandableNotificationRow row = null; |
| while (p != null) { |
| if (p instanceof ExpandableNotificationRow) { |
| row = (ExpandableNotificationRow) p; |
| break; |
| } |
| p = p.getParent(); |
| } |
| |
| if (row == null) { |
| return false; |
| } |
| |
| row.setUserExpanded(true); |
| |
| if (!mLockscreenUserManager.shouldAllowLockscreenRemoteInput()) { |
| final int userId = pendingIntent.getCreatorUserHandle().getIdentifier(); |
| if (mLockscreenUserManager.isLockscreenPublicMode(userId)) { |
| mCallback.onLockedRemoteInput(row, view); |
| return true; |
| } |
| if (mUserManager.getUserInfo(userId).isManagedProfile() |
| && mKeyguardManager.isDeviceLocked(userId)) { |
| mCallback.onLockedWorkRemoteInput(userId, row, view); |
| return true; |
| } |
| } |
| |
| if (riv == null) { |
| riv = findRemoteInputView(row.getPrivateLayout().getExpandedChild()); |
| if (riv == null) { |
| return false; |
| } |
| if (!row.getPrivateLayout().getExpandedChild().isShown()) { |
| mCallback.onMakeExpandedVisibleForRemoteInput(row, view); |
| return true; |
| } |
| } |
| |
| int width = view.getWidth(); |
| if (view instanceof TextView) { |
| // Center the reveal on the text which might be off-center from the TextView |
| TextView tv = (TextView) view; |
| if (tv.getLayout() != null) { |
| int innerWidth = (int) tv.getLayout().getLineWidth(0); |
| innerWidth += tv.getCompoundPaddingLeft() + tv.getCompoundPaddingRight(); |
| width = Math.min(width, innerWidth); |
| } |
| } |
| int cx = view.getLeft() + width / 2; |
| int cy = view.getTop() + view.getHeight() / 2; |
| int w = riv.getWidth(); |
| int h = riv.getHeight(); |
| int r = Math.max( |
| Math.max(cx + cy, cx + (h - cy)), |
| Math.max((w - cx) + cy, (w - cx) + (h - cy))); |
| |
| riv.setRevealParameters(cx, cy, r); |
| riv.setPendingIntent(pendingIntent); |
| riv.setRemoteInput(inputs, input); |
| riv.focusAnimated(); |
| |
| return true; |
| } |
| |
| private RemoteInputView findRemoteInputView(View v) { |
| if (v == null) { |
| return null; |
| } |
| return (RemoteInputView) v.findViewWithTag(RemoteInputView.VIEW_TAG); |
| } |
| }; |
| |
| private ShadeController getShadeController() { |
| if (mShadeController == null) { |
| mShadeController = Dependency.get(ShadeController.class); |
| } |
| return mShadeController; |
| } |
| |
| public NotificationRemoteInputManager(Context context) { |
| mContext = context; |
| mBarService = IStatusBarService.Stub.asInterface( |
| ServiceManager.getService(Context.STATUS_BAR_SERVICE)); |
| mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); |
| addLifetimeExtenders(); |
| mKeyguardManager = context.getSystemService(KeyguardManager.class); |
| } |
| |
| public void setUpWithPresenter(NotificationPresenter presenter, |
| Callback callback, |
| RemoteInputController.Delegate delegate) { |
| mPresenter = presenter; |
| mCallback = callback; |
| mRemoteInputController = new RemoteInputController(delegate); |
| mRemoteInputController.addCallback(new RemoteInputController.Callback() { |
| @Override |
| public void onRemoteInputSent(NotificationData.Entry entry) { |
| if (FORCE_REMOTE_INPUT_HISTORY |
| && isNotificationKeptForRemoteInputHistory(entry.key)) { |
| mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key); |
| } else if (mEntriesKeptForRemoteInputActive.contains(entry)) { |
| // We're currently holding onto this notification, but from the apps point of |
| // view it is already canceled, so we'll need to cancel it on the apps behalf |
| // after sending - unless the app posts an update in the mean time, so wait a |
| // bit. |
| Dependency.get(Dependency.MAIN_HANDLER).postDelayed(() -> { |
| if (mEntriesKeptForRemoteInputActive.remove(entry)) { |
| mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key); |
| } |
| }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY); |
| } |
| try { |
| mBarService.onNotificationDirectReplied(entry.notification.getKey()); |
| } catch (RemoteException e) { |
| // Nothing to do, system going down |
| } |
| } |
| }); |
| mSmartReplyController.setCallback((entry, reply) -> { |
| StatusBarNotification newSbn = |
| rebuildNotificationWithRemoteInput(entry, reply, true /* showSpinner */); |
| mEntryManager.updateNotification(newSbn, null /* ranking */); |
| }); |
| } |
| |
| /** |
| * Adds all the notification lifetime extenders. Each extender represents a reason for the |
| * NotificationRemoteInputManager to keep a notification lifetime extended. |
| */ |
| protected void addLifetimeExtenders() { |
| mLifetimeExtenders.add(new RemoteInputHistoryExtender()); |
| mLifetimeExtenders.add(new SmartReplyHistoryExtender()); |
| mLifetimeExtenders.add(new RemoteInputActiveExtender()); |
| } |
| |
| public ArrayList<NotificationLifetimeExtender> getLifetimeExtenders() { |
| return mLifetimeExtenders; |
| } |
| |
| public RemoteInputController getController() { |
| return mRemoteInputController; |
| } |
| |
| public void onPerformRemoveNotification(StatusBarNotification n, |
| NotificationData.Entry entry) { |
| if (mKeysKeptForRemoteInputHistory.contains(n.getKey())) { |
| mKeysKeptForRemoteInputHistory.remove(n.getKey()); |
| } |
| if (mRemoteInputController.isRemoteInputActive(entry)) { |
| mRemoteInputController.removeRemoteInput(entry, null); |
| } |
| } |
| |
| public void onPanelCollapsed() { |
| for (int i = 0; i < mEntriesKeptForRemoteInputActive.size(); i++) { |
| NotificationData.Entry entry = mEntriesKeptForRemoteInputActive.valueAt(i); |
| mRemoteInputController.removeRemoteInput(entry, null); |
| if (mNotificationLifetimeFinishedCallback != null) { |
| mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key); |
| } |
| } |
| mEntriesKeptForRemoteInputActive.clear(); |
| } |
| |
| public boolean isNotificationKeptForRemoteInputHistory(String key) { |
| return mKeysKeptForRemoteInputHistory.contains(key); |
| } |
| |
| public boolean shouldKeepForRemoteInputHistory(NotificationData.Entry entry) { |
| if (entry.row == null || entry.row.isDismissed()) { |
| return false; |
| } |
| if (!FORCE_REMOTE_INPUT_HISTORY) { |
| return false; |
| } |
| return (mRemoteInputController.isSpinning(entry.key) || entry.hasJustSentRemoteInput()); |
| } |
| |
| public boolean shouldKeepForSmartReplyHistory(NotificationData.Entry entry) { |
| if (entry.row == null || entry.row.isDismissed()) { |
| return false; |
| } |
| if (!FORCE_REMOTE_INPUT_HISTORY) { |
| return false; |
| } |
| return mSmartReplyController.isSendingSmartReply(entry.key); |
| } |
| |
| public void checkRemoteInputOutside(MotionEvent event) { |
| if (event.getAction() == MotionEvent.ACTION_OUTSIDE // touch outside the source bar |
| && event.getX() == 0 && event.getY() == 0 // a touch outside both bars |
| && mRemoteInputController.isRemoteInputActive()) { |
| mRemoteInputController.closeRemoteInputs(); |
| } |
| } |
| |
| @VisibleForTesting |
| StatusBarNotification rebuildNotificationForCanceledSmartReplies( |
| NotificationData.Entry entry) { |
| return rebuildNotificationWithRemoteInput(entry, null /* remoteInputTest */, |
| false /* showSpinner */); |
| } |
| |
| @VisibleForTesting |
| StatusBarNotification rebuildNotificationWithRemoteInput(NotificationData.Entry entry, |
| CharSequence remoteInputText, boolean showSpinner) { |
| StatusBarNotification sbn = entry.notification; |
| |
| Notification.Builder b = Notification.Builder |
| .recoverBuilder(mContext, sbn.getNotification().clone()); |
| if (remoteInputText != null) { |
| CharSequence[] oldHistory = sbn.getNotification().extras |
| .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY); |
| CharSequence[] newHistory; |
| if (oldHistory == null) { |
| newHistory = new CharSequence[1]; |
| } else { |
| newHistory = new CharSequence[oldHistory.length + 1]; |
| System.arraycopy(oldHistory, 0, newHistory, 1, oldHistory.length); |
| } |
| newHistory[0] = String.valueOf(remoteInputText); |
| b.setRemoteInputHistory(newHistory); |
| } |
| b.setShowRemoteInputSpinner(showSpinner); |
| b.setHideSmartReplies(true); |
| |
| Notification newNotification = b.build(); |
| |
| // Undo any compatibility view inflation |
| newNotification.contentView = sbn.getNotification().contentView; |
| newNotification.bigContentView = sbn.getNotification().bigContentView; |
| newNotification.headsUpContentView = sbn.getNotification().headsUpContentView; |
| |
| return new StatusBarNotification( |
| sbn.getPackageName(), |
| sbn.getOpPkg(), |
| sbn.getId(), |
| sbn.getTag(), |
| sbn.getUid(), |
| sbn.getInitialPid(), |
| newNotification, |
| sbn.getUser(), |
| sbn.getOverrideGroupKey(), |
| sbn.getPostTime()); |
| } |
| |
| @Override |
| public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { |
| pw.println("NotificationRemoteInputManager state:"); |
| pw.print(" mKeysKeptForRemoteInputHistory: "); |
| pw.println(mKeysKeptForRemoteInputHistory); |
| pw.print(" mEntriesKeptForRemoteInputActive: "); |
| pw.println(mEntriesKeptForRemoteInputActive); |
| } |
| |
| public void bindRow(ExpandableNotificationRow row) { |
| row.setRemoteInputController(mRemoteInputController); |
| row.setRemoteViewClickHandler(mOnClickHandler); |
| } |
| |
| @VisibleForTesting |
| public Set<NotificationData.Entry> getEntriesKeptForRemoteInputActive() { |
| return mEntriesKeptForRemoteInputActive; |
| } |
| |
| /** |
| * NotificationRemoteInputManager has multiple reasons to keep notification lifetime extended |
| * so we implement multiple NotificationLifetimeExtenders |
| */ |
| protected abstract class RemoteInputExtender implements NotificationLifetimeExtender { |
| @Override |
| public void setCallback(NotificationSafeToRemoveCallback callback) { |
| if (mNotificationLifetimeFinishedCallback == null) { |
| mNotificationLifetimeFinishedCallback = callback; |
| } |
| } |
| } |
| |
| /** |
| * Notification is kept alive as it was cancelled in response to a remote input interaction. |
| * This allows us to show what you replied and allows you to continue typing into it. |
| */ |
| protected class RemoteInputHistoryExtender extends RemoteInputExtender { |
| @Override |
| public boolean shouldExtendLifetime(@NonNull NotificationData.Entry entry) { |
| return shouldKeepForRemoteInputHistory(entry); |
| } |
| |
| @Override |
| public void setShouldManageLifetime(NotificationData.Entry entry, |
| boolean shouldExtend) { |
| if (shouldExtend) { |
| CharSequence remoteInputText = entry.remoteInputText; |
| if (TextUtils.isEmpty(remoteInputText)) { |
| remoteInputText = entry.remoteInputTextWhenReset; |
| } |
| StatusBarNotification newSbn = rebuildNotificationWithRemoteInput(entry, |
| remoteInputText, false /* showSpinner */); |
| entry.onRemoteInputInserted(); |
| |
| if (newSbn == null) { |
| return; |
| } |
| |
| mEntryManager.updateNotification(newSbn, null); |
| |
| // Ensure the entry hasn't already been removed. This can happen if there is an |
| // inflation exception while updating the remote history |
| if (entry.row == null || entry.row.isRemoved()) { |
| return; |
| } |
| |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "Keeping notification around after sending remote input " |
| + entry.key); |
| } |
| |
| mKeysKeptForRemoteInputHistory.add(entry.key); |
| } else { |
| mKeysKeptForRemoteInputHistory.remove(entry.key); |
| } |
| } |
| } |
| |
| /** |
| * Notification is kept alive for smart reply history. Similar to REMOTE_INPUT_HISTORY but with |
| * {@link SmartReplyController} specific logic |
| */ |
| protected class SmartReplyHistoryExtender extends RemoteInputExtender { |
| @Override |
| public boolean shouldExtendLifetime(@NonNull NotificationData.Entry entry) { |
| return shouldKeepForSmartReplyHistory(entry); |
| } |
| |
| @Override |
| public void setShouldManageLifetime(NotificationData.Entry entry, |
| boolean shouldExtend) { |
| if (shouldExtend) { |
| StatusBarNotification newSbn = rebuildNotificationForCanceledSmartReplies(entry); |
| |
| if (newSbn == null) { |
| return; |
| } |
| |
| mEntryManager.updateNotification(newSbn, null); |
| |
| if (entry.row == null || entry.row.isRemoved()) { |
| return; |
| } |
| |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "Keeping notification around after sending smart reply " |
| + entry.key); |
| } |
| |
| mKeysKeptForRemoteInputHistory.add(entry.key); |
| } else { |
| mKeysKeptForRemoteInputHistory.remove(entry.key); |
| mSmartReplyController.stopSending(entry); |
| } |
| } |
| } |
| |
| /** |
| * Notification is kept alive because the user is still using the remote input |
| */ |
| protected class RemoteInputActiveExtender extends RemoteInputExtender { |
| @Override |
| public boolean shouldExtendLifetime(@NonNull NotificationData.Entry entry) { |
| if (entry.row == null || entry.row.isDismissed()) { |
| return false; |
| } |
| return mRemoteInputController.isRemoteInputActive(entry); |
| } |
| |
| @Override |
| public void setShouldManageLifetime(NotificationData.Entry entry, |
| boolean shouldExtend) { |
| if (shouldExtend) { |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "Keeping notification around while remote input active " |
| + entry.key); |
| } |
| mEntriesKeptForRemoteInputActive.add(entry); |
| } else { |
| mEntriesKeptForRemoteInputActive.remove(entry); |
| } |
| } |
| } |
| |
| /** |
| * Callback for various remote input related events, or for providing information that |
| * NotificationRemoteInputManager needs to know to decide what to do. |
| */ |
| public interface Callback { |
| |
| /** |
| * Called when remote input was activated but the device is locked. |
| * |
| * @param row |
| * @param clicked |
| */ |
| void onLockedRemoteInput(ExpandableNotificationRow row, View clicked); |
| |
| /** |
| * Called when remote input was activated but the device is locked and in a managed profile. |
| * |
| * @param userId |
| * @param row |
| * @param clicked |
| */ |
| void onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row, View clicked); |
| |
| /** |
| * Called when a row should be made expanded for the purposes of remote input. |
| * |
| * @param row |
| * @param clickedView |
| */ |
| void onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, View clickedView); |
| |
| /** |
| * Return whether or not remote input should be handled for this view. |
| * |
| * @param view |
| * @param pendingIntent |
| * @return true iff the remote input should be handled |
| */ |
| boolean shouldHandleRemoteInput(View view, PendingIntent pendingIntent); |
| |
| /** |
| * Performs any special handling for a remote view click. The default behaviour can be |
| * called through the defaultHandler parameter. |
| * |
| * @param view |
| * @param pendingIntent |
| * @param fillInIntent |
| * @param defaultHandler |
| * @return true iff the click was handled |
| */ |
| boolean handleRemoteViewClick(View view, PendingIntent pendingIntent, Intent fillInIntent, |
| ClickHandler defaultHandler); |
| } |
| |
| /** |
| * Helper interface meant for passing the default on click behaviour to NotificationPresenter, |
| * so it may do its own handling before invoking the default behaviour. |
| */ |
| public interface ClickHandler { |
| /** |
| * Tries to handle a click on a remote view. |
| * |
| * @return true iff the click was handled |
| */ |
| boolean handleClick(); |
| } |
| } |