| /* |
| * 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.app.ActivityManager; |
| 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.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.statusbar.policy.RemoteInputView; |
| |
| import java.io.FileDescriptor; |
| import java.io.PrintWriter; |
| 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 final boolean FORCE_REMOTE_INPUT_HISTORY = |
| SystemProperties.getBoolean("debug.force_remoteinput_history", true); |
| private static final boolean DEBUG = false; |
| private static final String TAG = "NotificationRemoteInputManager"; |
| |
| /** |
| * 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; |
| |
| protected final ArraySet<NotificationData.Entry> mRemoteInputEntriesToRemoveOnCollapse = |
| new ArraySet<>(); |
| |
| // Dependencies: |
| protected final NotificationLockscreenUserManager mLockscreenUserManager = |
| Dependency.get(NotificationLockscreenUserManager.class); |
| |
| protected final Context mContext; |
| private final UserManager mUserManager; |
| |
| protected RemoteInputController mRemoteInputController; |
| protected NotificationPresenter mPresenter; |
| protected NotificationEntryManager mEntryManager; |
| protected IStatusBarService mBarService; |
| protected Callback mCallback; |
| |
| private final RemoteViews.OnClickHandler mOnClickHandler = new RemoteViews.OnClickHandler() { |
| |
| @Override |
| public boolean onClickHandler( |
| final View view, final PendingIntent pendingIntent, final Intent fillInIntent) { |
| mPresenter.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, |
| () -> superOnClickHandler(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; |
| } |
| |
| private boolean superOnClickHandler(View view, PendingIntent pendingIntent, |
| Intent fillInIntent) { |
| return super.onClickHandler(view, pendingIntent, fillInIntent, |
| WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY); |
| } |
| |
| 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() |
| && mPresenter.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); |
| } |
| }; |
| |
| public NotificationRemoteInputManager(Context context) { |
| mContext = context; |
| mBarService = IStatusBarService.Stub.asInterface( |
| ServiceManager.getService(Context.STATUS_BAR_SERVICE)); |
| mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); |
| } |
| |
| public void setUpWithPresenter(NotificationPresenter presenter, |
| NotificationEntryManager entryManager, |
| Callback callback, |
| RemoteInputController.Delegate delegate) { |
| mPresenter = presenter; |
| mEntryManager = entryManager; |
| mCallback = callback; |
| mRemoteInputController = new RemoteInputController(delegate); |
| mRemoteInputController.addCallback(new RemoteInputController.Callback() { |
| @Override |
| public void onRemoteInputSent(NotificationData.Entry entry) { |
| if (FORCE_REMOTE_INPUT_HISTORY |
| && mEntryManager.isNotificationKeptForRemoteInput(entry.key)) { |
| mEntryManager.removeNotification(entry.key, null); |
| } else if (mRemoteInputEntriesToRemoveOnCollapse.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. |
| mPresenter.getHandler().postDelayed(() -> { |
| if (mRemoteInputEntriesToRemoveOnCollapse.remove(entry)) { |
| mEntryManager.removeNotification(entry.key, null); |
| } |
| }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY); |
| } |
| try { |
| mBarService.onNotificationDirectReplied(entry.notification.getKey()); |
| } catch (RemoteException e) { |
| // Nothing to do, system going down |
| } |
| } |
| }); |
| |
| } |
| |
| public RemoteInputController getController() { |
| return mRemoteInputController; |
| } |
| |
| public void onUpdateNotification(NotificationData.Entry entry) { |
| mRemoteInputEntriesToRemoveOnCollapse.remove(entry); |
| } |
| |
| /** |
| * Returns true if NotificationRemoteInputManager wants to keep this notification around. |
| * |
| * @param entry notification being removed |
| */ |
| public boolean onRemoveNotification(NotificationData.Entry entry) { |
| if (entry != null && mRemoteInputController.isRemoteInputActive(entry) |
| && (entry.row != null && !entry.row.isDismissed())) { |
| mRemoteInputEntriesToRemoveOnCollapse.add(entry); |
| return true; |
| } |
| return false; |
| } |
| |
| public void onPerformRemoveNotification(StatusBarNotification n, |
| NotificationData.Entry entry) { |
| if (mRemoteInputController.isRemoteInputActive(entry)) { |
| mRemoteInputController.removeRemoteInput(entry, null); |
| } |
| } |
| |
| public void removeRemoteInputEntriesKeptUntilCollapsed() { |
| for (int i = 0; i < mRemoteInputEntriesToRemoveOnCollapse.size(); i++) { |
| NotificationData.Entry entry = mRemoteInputEntriesToRemoveOnCollapse.valueAt(i); |
| mRemoteInputController.removeRemoteInput(entry, null); |
| mEntryManager.removeNotification(entry.key, mEntryManager.getLatestRankingMap()); |
| } |
| mRemoteInputEntriesToRemoveOnCollapse.clear(); |
| } |
| |
| 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(); |
| } |
| } |
| |
| @Override |
| public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { |
| pw.println("NotificationRemoteInputManager state:"); |
| pw.print(" mRemoteInputEntriesToRemoveOnCollapse: "); |
| pw.println(mRemoteInputEntriesToRemoveOnCollapse); |
| } |
| |
| public void bindRow(ExpandableNotificationRow row) { |
| row.setRemoteInputController(mRemoteInputController); |
| row.setRemoteViewClickHandler(mOnClickHandler); |
| } |
| |
| @VisibleForTesting |
| public Set<NotificationData.Entry> getRemoteInputEntriesToRemoveOnCollapse() { |
| return mRemoteInputEntriesToRemoveOnCollapse; |
| } |
| |
| /** |
| * 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(); |
| } |
| } |