Home | History | Annotate | Download | only in statusbar
      1 /*
      2  * Copyright (C) 2015 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 
     17 package com.android.systemui.statusbar;
     18 
     19 import android.app.Notification;
     20 import android.app.RemoteInput;
     21 import android.content.Context;
     22 import android.os.SystemProperties;
     23 import android.util.ArrayMap;
     24 import android.util.Pair;
     25 
     26 import com.android.internal.util.Preconditions;
     27 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
     28 import com.android.systemui.statusbar.policy.RemoteInputView;
     29 
     30 import java.lang.ref.WeakReference;
     31 import java.util.ArrayList;
     32 import java.util.List;
     33 
     34 /**
     35  * Keeps track of the currently active {@link RemoteInputView}s.
     36  */
     37 public class RemoteInputController {
     38     private static final boolean ENABLE_REMOTE_INPUT =
     39             SystemProperties.getBoolean("debug.enable_remote_input", true);
     40 
     41     private final ArrayList<Pair<WeakReference<NotificationEntry>, Object>> mOpen
     42             = new ArrayList<>();
     43     private final ArrayMap<String, Object> mSpinning = new ArrayMap<>();
     44     private final ArrayList<Callback> mCallbacks = new ArrayList<>(3);
     45     private final Delegate mDelegate;
     46 
     47     public RemoteInputController(Delegate delegate) {
     48         mDelegate = delegate;
     49     }
     50 
     51     /**
     52      * Adds RemoteInput actions from the WearableExtender; to be removed once more apps support this
     53      * via first-class API.
     54      *
     55      * TODO: Remove once enough apps specify remote inputs on their own.
     56      */
     57     public static void processForRemoteInput(Notification n, Context context) {
     58         if (!ENABLE_REMOTE_INPUT) {
     59             return;
     60         }
     61 
     62         if (n.extras != null && n.extras.containsKey("android.wearable.EXTENSIONS") &&
     63                 (n.actions == null || n.actions.length == 0)) {
     64             Notification.Action viableAction = null;
     65             Notification.WearableExtender we = new Notification.WearableExtender(n);
     66 
     67             List<Notification.Action> actions = we.getActions();
     68             final int numActions = actions.size();
     69 
     70             for (int i = 0; i < numActions; i++) {
     71                 Notification.Action action = actions.get(i);
     72                 if (action == null) {
     73                     continue;
     74                 }
     75                 RemoteInput[] remoteInputs = action.getRemoteInputs();
     76                 if (remoteInputs == null) {
     77                     continue;
     78                 }
     79                 for (RemoteInput ri : remoteInputs) {
     80                     if (ri.getAllowFreeFormInput()) {
     81                         viableAction = action;
     82                         break;
     83                     }
     84                 }
     85                 if (viableAction != null) {
     86                     break;
     87                 }
     88             }
     89 
     90             if (viableAction != null) {
     91                 Notification.Builder rebuilder = Notification.Builder.recoverBuilder(context, n);
     92                 rebuilder.setActions(viableAction);
     93                 rebuilder.build(); // will rewrite n
     94             }
     95         }
     96     }
     97 
     98     /**
     99      * Adds a currently active remote input.
    100      *
    101      * @param entry the entry for which a remote input is now active.
    102      * @param token a token identifying the view that is managing the remote input
    103      */
    104     public void addRemoteInput(NotificationEntry entry, Object token) {
    105         Preconditions.checkNotNull(entry);
    106         Preconditions.checkNotNull(token);
    107 
    108         boolean found = pruneWeakThenRemoveAndContains(
    109                 entry /* contains */, null /* remove */, token /* removeToken */);
    110         if (!found) {
    111             mOpen.add(new Pair<>(new WeakReference<>(entry), token));
    112         }
    113 
    114         apply(entry);
    115     }
    116 
    117     /**
    118      * Removes a currently active remote input.
    119      *
    120      * @param entry the entry for which a remote input should be removed.
    121      * @param token a token identifying the view that is requesting the removal. If non-null,
    122      *              the entry is only removed if the token matches the last added token for this
    123      *              entry. If null, the entry is removed regardless.
    124      */
    125     public void removeRemoteInput(NotificationEntry entry, Object token) {
    126         Preconditions.checkNotNull(entry);
    127 
    128         pruneWeakThenRemoveAndContains(null /* contains */, entry /* remove */, token);
    129 
    130         apply(entry);
    131     }
    132 
    133     /**
    134      * Adds a currently spinning (i.e. sending) remote input.
    135      *
    136      * @param key the key of the entry that's spinning.
    137      * @param token the token of the view managing the remote input.
    138      */
    139     public void addSpinning(String key, Object token) {
    140         Preconditions.checkNotNull(key);
    141         Preconditions.checkNotNull(token);
    142 
    143         mSpinning.put(key, token);
    144     }
    145 
    146     /**
    147      * Removes a currently spinning remote input.
    148      *
    149      * @param key the key of the entry for which a remote input should be removed.
    150      * @param token a token identifying the view that is requesting the removal. If non-null,
    151      *              the entry is only removed if the token matches the last added token for this
    152      *              entry. If null, the entry is removed regardless.
    153      */
    154     public void removeSpinning(String key, Object token) {
    155         Preconditions.checkNotNull(key);
    156 
    157         if (token == null || mSpinning.get(key) == token) {
    158             mSpinning.remove(key);
    159         }
    160     }
    161 
    162     public boolean isSpinning(String key) {
    163         return mSpinning.containsKey(key);
    164     }
    165 
    166     /**
    167      * Same as {@link #isSpinning}, but also verifies that the token is the same
    168      * @param key the key that is spinning
    169      * @param token the token that needs to be the same
    170      * @return if this key with a given token is spinning
    171      */
    172     public boolean isSpinning(String key, Object token) {
    173         return mSpinning.get(key) == token;
    174     }
    175 
    176     private void apply(NotificationEntry entry) {
    177         mDelegate.setRemoteInputActive(entry, isRemoteInputActive(entry));
    178         boolean remoteInputActive = isRemoteInputActive();
    179         int N = mCallbacks.size();
    180         for (int i = 0; i < N; i++) {
    181             mCallbacks.get(i).onRemoteInputActive(remoteInputActive);
    182         }
    183     }
    184 
    185     /**
    186      * @return true if {@param entry} has an active RemoteInput
    187      */
    188     public boolean isRemoteInputActive(NotificationEntry entry) {
    189         return pruneWeakThenRemoveAndContains(entry /* contains */, null /* remove */,
    190                 null /* removeToken */);
    191     }
    192 
    193     /**
    194      * @return true if any entry has an active RemoteInput
    195      */
    196     public boolean isRemoteInputActive() {
    197         pruneWeakThenRemoveAndContains(null /* contains */, null /* remove */,
    198                 null /* removeToken */);
    199         return !mOpen.isEmpty();
    200     }
    201 
    202     /**
    203      * Prunes dangling weak references, removes entries referring to {@param remove} and returns
    204      * whether {@param contains} is part of the array in a single loop.
    205      * @param remove if non-null, removes this entry from the active remote inputs
    206      * @param removeToken if non-null, only removes an entry if this matches the token when the
    207      *                    entry was added.
    208      * @return true if {@param contains} is in the set of active remote inputs
    209      */
    210     private boolean pruneWeakThenRemoveAndContains(
    211             NotificationEntry contains, NotificationEntry remove, Object removeToken) {
    212         boolean found = false;
    213         for (int i = mOpen.size() - 1; i >= 0; i--) {
    214             NotificationEntry item = mOpen.get(i).first.get();
    215             Object itemToken = mOpen.get(i).second;
    216             boolean removeTokenMatches = (removeToken == null || itemToken == removeToken);
    217 
    218             if (item == null || (item == remove && removeTokenMatches)) {
    219                 mOpen.remove(i);
    220             } else if (item == contains) {
    221                 if (removeToken != null && removeToken != itemToken) {
    222                     // We need to update the token. Remove here and let caller reinsert it.
    223                     mOpen.remove(i);
    224                 } else {
    225                     found = true;
    226                 }
    227             }
    228         }
    229         return found;
    230     }
    231 
    232 
    233     public void addCallback(Callback callback) {
    234         Preconditions.checkNotNull(callback);
    235         mCallbacks.add(callback);
    236     }
    237 
    238     public void remoteInputSent(NotificationEntry entry) {
    239         int N = mCallbacks.size();
    240         for (int i = 0; i < N; i++) {
    241             mCallbacks.get(i).onRemoteInputSent(entry);
    242         }
    243     }
    244 
    245     public void closeRemoteInputs() {
    246         if (mOpen.size() == 0) {
    247             return;
    248         }
    249 
    250         // Make a copy because closing the remote inputs will modify mOpen.
    251         ArrayList<NotificationEntry> list = new ArrayList<>(mOpen.size());
    252         for (int i = mOpen.size() - 1; i >= 0; i--) {
    253             NotificationEntry entry = mOpen.get(i).first.get();
    254             if (entry != null && entry.rowExists()) {
    255                 list.add(entry);
    256             }
    257         }
    258 
    259         for (int i = list.size() - 1; i >= 0; i--) {
    260             NotificationEntry entry = list.get(i);
    261             if (entry.rowExists()) {
    262                 entry.closeRemoteInput();
    263             }
    264         }
    265     }
    266 
    267     public void requestDisallowLongPressAndDismiss() {
    268         mDelegate.requestDisallowLongPressAndDismiss();
    269     }
    270 
    271     public void lockScrollTo(NotificationEntry entry) {
    272         mDelegate.lockScrollTo(entry);
    273     }
    274 
    275     public interface Callback {
    276         default void onRemoteInputActive(boolean active) {}
    277 
    278         default void onRemoteInputSent(NotificationEntry entry) {}
    279     }
    280 
    281     public interface Delegate {
    282         /**
    283          * Activate remote input if necessary.
    284          */
    285         void setRemoteInputActive(NotificationEntry entry, boolean remoteInputActive);
    286 
    287         /**
    288          * Request that the view does not dismiss nor perform long press for the current touch.
    289          */
    290         void requestDisallowLongPressAndDismiss();
    291 
    292         /**
    293          * Request that the view is made visible by scrolling to it, and keep the scroll locked until
    294          * the user scrolls, or {@param entry} loses focus or is detached.
    295          */
    296         void lockScrollTo(NotificationEntry entry);
    297     }
    298 }
    299