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