Home | History | Annotate | Download | only in statusbar
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 package com.android.systemui.statusbar;
     17 
     18 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY;
     19 
     20 import android.app.ActivityManager;
     21 import android.app.PendingIntent;
     22 import android.app.RemoteInput;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.os.RemoteException;
     26 import android.os.ServiceManager;
     27 import android.os.SystemClock;
     28 import android.os.SystemProperties;
     29 import android.os.UserManager;
     30 import android.service.notification.StatusBarNotification;
     31 import android.util.ArraySet;
     32 import android.util.Log;
     33 import android.view.MotionEvent;
     34 import android.view.View;
     35 import android.view.ViewGroup;
     36 import android.view.ViewParent;
     37 import android.widget.RemoteViews;
     38 import android.widget.TextView;
     39 
     40 import com.android.internal.annotations.VisibleForTesting;
     41 import com.android.internal.statusbar.IStatusBarService;
     42 import com.android.internal.statusbar.NotificationVisibility;
     43 import com.android.systemui.Dependency;
     44 import com.android.systemui.Dumpable;
     45 import com.android.systemui.statusbar.policy.RemoteInputView;
     46 
     47 import java.io.FileDescriptor;
     48 import java.io.PrintWriter;
     49 import java.util.Set;
     50 
     51 /**
     52  * Class for handling remote input state over a set of notifications. This class handles things
     53  * like keeping notifications temporarily that were cancelled as a response to a remote input
     54  * interaction, keeping track of notifications to remove when NotificationPresenter is collapsed,
     55  * and handling clicks on remote views.
     56  */
     57 public class NotificationRemoteInputManager implements Dumpable {
     58     public static final boolean ENABLE_REMOTE_INPUT =
     59             SystemProperties.getBoolean("debug.enable_remote_input", true);
     60     public static final boolean FORCE_REMOTE_INPUT_HISTORY =
     61             SystemProperties.getBoolean("debug.force_remoteinput_history", true);
     62     private static final boolean DEBUG = false;
     63     private static final String TAG = "NotificationRemoteInputManager";
     64 
     65     /**
     66      * How long to wait before auto-dismissing a notification that was kept for remote input, and
     67      * has now sent a remote input. We auto-dismiss, because the app may not see a reason to cancel
     68      * these given that they technically don't exist anymore. We wait a bit in case the app issues
     69      * an update.
     70      */
     71     private static final int REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY = 200;
     72 
     73     protected final ArraySet<NotificationData.Entry> mRemoteInputEntriesToRemoveOnCollapse =
     74             new ArraySet<>();
     75 
     76     // Dependencies:
     77     protected final NotificationLockscreenUserManager mLockscreenUserManager =
     78             Dependency.get(NotificationLockscreenUserManager.class);
     79 
     80     protected final Context mContext;
     81     private final UserManager mUserManager;
     82 
     83     protected RemoteInputController mRemoteInputController;
     84     protected NotificationPresenter mPresenter;
     85     protected NotificationEntryManager mEntryManager;
     86     protected IStatusBarService mBarService;
     87     protected Callback mCallback;
     88 
     89     private final RemoteViews.OnClickHandler mOnClickHandler = new RemoteViews.OnClickHandler() {
     90 
     91         @Override
     92         public boolean onClickHandler(
     93                 final View view, final PendingIntent pendingIntent, final Intent fillInIntent) {
     94             mPresenter.wakeUpIfDozing(SystemClock.uptimeMillis(), view);
     95 
     96             if (handleRemoteInput(view, pendingIntent)) {
     97                 return true;
     98             }
     99 
    100             if (DEBUG) {
    101                 Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent);
    102             }
    103             logActionClick(view);
    104             // The intent we are sending is for the application, which
    105             // won't have permission to immediately start an activity after
    106             // the user switches to home.  We know it is safe to do at this
    107             // point, so make sure new activity switches are now allowed.
    108             try {
    109                 ActivityManager.getService().resumeAppSwitches();
    110             } catch (RemoteException e) {
    111             }
    112             return mCallback.handleRemoteViewClick(view, pendingIntent, fillInIntent,
    113                     () -> superOnClickHandler(view, pendingIntent, fillInIntent));
    114         }
    115 
    116         private void logActionClick(View view) {
    117             ViewParent parent = view.getParent();
    118             String key = getNotificationKeyForParent(parent);
    119             if (key == null) {
    120                 Log.w(TAG, "Couldn't determine notification for click.");
    121                 return;
    122             }
    123             int index = -1;
    124             // If this is a default template, determine the index of the button.
    125             if (view.getId() == com.android.internal.R.id.action0 &&
    126                     parent != null && parent instanceof ViewGroup) {
    127                 ViewGroup actionGroup = (ViewGroup) parent;
    128                 index = actionGroup.indexOfChild(view);
    129             }
    130             final int count = mEntryManager.getNotificationData().getActiveNotifications().size();
    131             final int rank = mEntryManager.getNotificationData().getRank(key);
    132             final NotificationVisibility nv = NotificationVisibility.obtain(key, rank, count, true);
    133             try {
    134                 mBarService.onNotificationActionClick(key, index, nv);
    135             } catch (RemoteException e) {
    136                 // Ignore
    137             }
    138         }
    139 
    140         private String getNotificationKeyForParent(ViewParent parent) {
    141             while (parent != null) {
    142                 if (parent instanceof ExpandableNotificationRow) {
    143                     return ((ExpandableNotificationRow) parent)
    144                             .getStatusBarNotification().getKey();
    145                 }
    146                 parent = parent.getParent();
    147             }
    148             return null;
    149         }
    150 
    151         private boolean superOnClickHandler(View view, PendingIntent pendingIntent,
    152                 Intent fillInIntent) {
    153             return super.onClickHandler(view, pendingIntent, fillInIntent,
    154                     WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY);
    155         }
    156 
    157         private boolean handleRemoteInput(View view, PendingIntent pendingIntent) {
    158             if (mCallback.shouldHandleRemoteInput(view, pendingIntent)) {
    159                 return true;
    160             }
    161 
    162             Object tag = view.getTag(com.android.internal.R.id.remote_input_tag);
    163             RemoteInput[] inputs = null;
    164             if (tag instanceof RemoteInput[]) {
    165                 inputs = (RemoteInput[]) tag;
    166             }
    167 
    168             if (inputs == null) {
    169                 return false;
    170             }
    171 
    172             RemoteInput input = null;
    173 
    174             for (RemoteInput i : inputs) {
    175                 if (i.getAllowFreeFormInput()) {
    176                     input = i;
    177                 }
    178             }
    179 
    180             if (input == null) {
    181                 return false;
    182             }
    183 
    184             ViewParent p = view.getParent();
    185             RemoteInputView riv = null;
    186             while (p != null) {
    187                 if (p instanceof View) {
    188                     View pv = (View) p;
    189                     if (pv.isRootNamespace()) {
    190                         riv = findRemoteInputView(pv);
    191                         break;
    192                     }
    193                 }
    194                 p = p.getParent();
    195             }
    196             ExpandableNotificationRow row = null;
    197             while (p != null) {
    198                 if (p instanceof ExpandableNotificationRow) {
    199                     row = (ExpandableNotificationRow) p;
    200                     break;
    201                 }
    202                 p = p.getParent();
    203             }
    204 
    205             if (row == null) {
    206                 return false;
    207             }
    208 
    209             row.setUserExpanded(true);
    210 
    211             if (!mLockscreenUserManager.shouldAllowLockscreenRemoteInput()) {
    212                 final int userId = pendingIntent.getCreatorUserHandle().getIdentifier();
    213                 if (mLockscreenUserManager.isLockscreenPublicMode(userId)) {
    214                     mCallback.onLockedRemoteInput(row, view);
    215                     return true;
    216                 }
    217                 if (mUserManager.getUserInfo(userId).isManagedProfile()
    218                         && mPresenter.isDeviceLocked(userId)) {
    219                     mCallback.onLockedWorkRemoteInput(userId, row, view);
    220                     return true;
    221                 }
    222             }
    223 
    224             if (riv == null) {
    225                 riv = findRemoteInputView(row.getPrivateLayout().getExpandedChild());
    226                 if (riv == null) {
    227                     return false;
    228                 }
    229                 if (!row.getPrivateLayout().getExpandedChild().isShown()) {
    230                     mCallback.onMakeExpandedVisibleForRemoteInput(row, view);
    231                     return true;
    232                 }
    233             }
    234 
    235             int width = view.getWidth();
    236             if (view instanceof TextView) {
    237                 // Center the reveal on the text which might be off-center from the TextView
    238                 TextView tv = (TextView) view;
    239                 if (tv.getLayout() != null) {
    240                     int innerWidth = (int) tv.getLayout().getLineWidth(0);
    241                     innerWidth += tv.getCompoundPaddingLeft() + tv.getCompoundPaddingRight();
    242                     width = Math.min(width, innerWidth);
    243                 }
    244             }
    245             int cx = view.getLeft() + width / 2;
    246             int cy = view.getTop() + view.getHeight() / 2;
    247             int w = riv.getWidth();
    248             int h = riv.getHeight();
    249             int r = Math.max(
    250                     Math.max(cx + cy, cx + (h - cy)),
    251                     Math.max((w - cx) + cy, (w - cx) + (h - cy)));
    252 
    253             riv.setRevealParameters(cx, cy, r);
    254             riv.setPendingIntent(pendingIntent);
    255             riv.setRemoteInput(inputs, input);
    256             riv.focusAnimated();
    257 
    258             return true;
    259         }
    260 
    261         private RemoteInputView findRemoteInputView(View v) {
    262             if (v == null) {
    263                 return null;
    264             }
    265             return (RemoteInputView) v.findViewWithTag(RemoteInputView.VIEW_TAG);
    266         }
    267     };
    268 
    269     public NotificationRemoteInputManager(Context context) {
    270         mContext = context;
    271         mBarService = IStatusBarService.Stub.asInterface(
    272                 ServiceManager.getService(Context.STATUS_BAR_SERVICE));
    273         mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
    274     }
    275 
    276     public void setUpWithPresenter(NotificationPresenter presenter,
    277             NotificationEntryManager entryManager,
    278             Callback callback,
    279             RemoteInputController.Delegate delegate) {
    280         mPresenter = presenter;
    281         mEntryManager = entryManager;
    282         mCallback = callback;
    283         mRemoteInputController = new RemoteInputController(delegate);
    284         mRemoteInputController.addCallback(new RemoteInputController.Callback() {
    285             @Override
    286             public void onRemoteInputSent(NotificationData.Entry entry) {
    287                 if (FORCE_REMOTE_INPUT_HISTORY
    288                         && mEntryManager.isNotificationKeptForRemoteInput(entry.key)) {
    289                     mEntryManager.removeNotification(entry.key, null);
    290                 } else if (mRemoteInputEntriesToRemoveOnCollapse.contains(entry)) {
    291                     // We're currently holding onto this notification, but from the apps point of
    292                     // view it is already canceled, so we'll need to cancel it on the apps behalf
    293                     // after sending - unless the app posts an update in the mean time, so wait a
    294                     // bit.
    295                     mPresenter.getHandler().postDelayed(() -> {
    296                         if (mRemoteInputEntriesToRemoveOnCollapse.remove(entry)) {
    297                             mEntryManager.removeNotification(entry.key, null);
    298                         }
    299                     }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
    300                 }
    301                 try {
    302                     mBarService.onNotificationDirectReplied(entry.notification.getKey());
    303                 } catch (RemoteException e) {
    304                     // Nothing to do, system going down
    305                 }
    306             }
    307         });
    308 
    309     }
    310 
    311     public RemoteInputController getController() {
    312         return mRemoteInputController;
    313     }
    314 
    315     public void onUpdateNotification(NotificationData.Entry entry) {
    316         mRemoteInputEntriesToRemoveOnCollapse.remove(entry);
    317     }
    318 
    319     /**
    320      * Returns true if NotificationRemoteInputManager wants to keep this notification around.
    321      *
    322      * @param entry notification being removed
    323      */
    324     public boolean onRemoveNotification(NotificationData.Entry entry) {
    325         if (entry != null && mRemoteInputController.isRemoteInputActive(entry)
    326                 && (entry.row != null && !entry.row.isDismissed())) {
    327             mRemoteInputEntriesToRemoveOnCollapse.add(entry);
    328             return true;
    329         }
    330         return false;
    331     }
    332 
    333     public void onPerformRemoveNotification(StatusBarNotification n,
    334             NotificationData.Entry entry) {
    335         if (mRemoteInputController.isRemoteInputActive(entry)) {
    336             mRemoteInputController.removeRemoteInput(entry, null);
    337         }
    338     }
    339 
    340     public void removeRemoteInputEntriesKeptUntilCollapsed() {
    341         for (int i = 0; i < mRemoteInputEntriesToRemoveOnCollapse.size(); i++) {
    342             NotificationData.Entry entry = mRemoteInputEntriesToRemoveOnCollapse.valueAt(i);
    343             mRemoteInputController.removeRemoteInput(entry, null);
    344             mEntryManager.removeNotification(entry.key, mEntryManager.getLatestRankingMap());
    345         }
    346         mRemoteInputEntriesToRemoveOnCollapse.clear();
    347     }
    348 
    349     public void checkRemoteInputOutside(MotionEvent event) {
    350         if (event.getAction() == MotionEvent.ACTION_OUTSIDE // touch outside the source bar
    351                 && event.getX() == 0 && event.getY() == 0  // a touch outside both bars
    352                 && mRemoteInputController.isRemoteInputActive()) {
    353             mRemoteInputController.closeRemoteInputs();
    354         }
    355     }
    356 
    357     @Override
    358     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
    359         pw.println("NotificationRemoteInputManager state:");
    360         pw.print("  mRemoteInputEntriesToRemoveOnCollapse: ");
    361         pw.println(mRemoteInputEntriesToRemoveOnCollapse);
    362     }
    363 
    364     public void bindRow(ExpandableNotificationRow row) {
    365         row.setRemoteInputController(mRemoteInputController);
    366         row.setRemoteViewClickHandler(mOnClickHandler);
    367     }
    368 
    369     @VisibleForTesting
    370     public Set<NotificationData.Entry> getRemoteInputEntriesToRemoveOnCollapse() {
    371         return mRemoteInputEntriesToRemoveOnCollapse;
    372     }
    373 
    374     /**
    375      * Callback for various remote input related events, or for providing information that
    376      * NotificationRemoteInputManager needs to know to decide what to do.
    377      */
    378     public interface Callback {
    379 
    380         /**
    381          * Called when remote input was activated but the device is locked.
    382          *
    383          * @param row
    384          * @param clicked
    385          */
    386         void onLockedRemoteInput(ExpandableNotificationRow row, View clicked);
    387 
    388         /**
    389          * Called when remote input was activated but the device is locked and in a managed profile.
    390          *
    391          * @param userId
    392          * @param row
    393          * @param clicked
    394          */
    395         void onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row, View clicked);
    396 
    397         /**
    398          * Called when a row should be made expanded for the purposes of remote input.
    399          *
    400          * @param row
    401          * @param clickedView
    402          */
    403         void onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, View clickedView);
    404 
    405         /**
    406          * Return whether or not remote input should be handled for this view.
    407          *
    408          * @param view
    409          * @param pendingIntent
    410          * @return true iff the remote input should be handled
    411          */
    412         boolean shouldHandleRemoteInput(View view, PendingIntent pendingIntent);
    413 
    414         /**
    415          * Performs any special handling for a remote view click. The default behaviour can be
    416          * called through the defaultHandler parameter.
    417          *
    418          * @param view
    419          * @param pendingIntent
    420          * @param fillInIntent
    421          * @param defaultHandler
    422          * @return  true iff the click was handled
    423          */
    424         boolean handleRemoteViewClick(View view, PendingIntent pendingIntent, Intent fillInIntent,
    425                 ClickHandler defaultHandler);
    426     }
    427 
    428     /**
    429      * Helper interface meant for passing the default on click behaviour to NotificationPresenter,
    430      * so it may do its own handling before invoking the default behaviour.
    431      */
    432     public interface ClickHandler {
    433         /**
    434          * Tries to handle a click on a remote view.
    435          *
    436          * @return true iff the click was handled
    437          */
    438         boolean handleClick();
    439     }
    440 }
    441