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.quickstep;
17 
18 import static com.android.launcher3.Utilities.postAsyncCallback;
19 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
20 
21 import android.os.Looper;
22 import android.util.Log;
23 import android.util.SparseArray;
24 
25 import androidx.annotation.NonNull;
26 import androidx.annotation.Nullable;
27 
28 import com.android.launcher3.config.FeatureFlags;
29 import com.android.quickstep.util.ActiveGestureErrorDetector;
30 import com.android.quickstep.util.ActiveGestureLog;
31 
32 import java.util.ArrayList;
33 import java.util.LinkedList;
34 import java.util.StringJoiner;
35 import java.util.function.Consumer;
36 
37 /**
38  * Utility class to help manage multiple callbacks based on different states.
39  */
40 public class MultiStateCallback {
41 
42     private static final String TAG = "MultiStateCallback";
43     public static final boolean DEBUG_STATES = false;
44 
45     private final SparseArray<LinkedList<Runnable>> mCallbacks = new SparseArray<>();
46     private final SparseArray<ArrayList<Consumer<Boolean>>> mStateChangeListeners =
47             new SparseArray<>();
48 
49     @NonNull private final TrackedEventsMapper mTrackedEventsMapper;
50 
51     private final String[] mStateNames;
52 
53     private int mState = 0;
54 
MultiStateCallback(String[] stateNames)55     public MultiStateCallback(String[] stateNames) {
56         this(stateNames, stateFlag -> null);
57     }
58 
MultiStateCallback( String[] stateNames, @NonNull TrackedEventsMapper trackedEventsMapper)59     public MultiStateCallback(
60             String[] stateNames,
61             @NonNull TrackedEventsMapper trackedEventsMapper) {
62         mStateNames = DEBUG_STATES ? stateNames : null;
63         mTrackedEventsMapper = trackedEventsMapper;
64     }
65 
66     /**
67      * Adds the provided state flags to the global state on the UI thread and executes any callbacks
68      * as a result.
69      *
70      * Also tracks the provided gesture events for error detection. Each provided event must be
71      * associated with one provided state flag.
72      */
setStateOnUiThread(int stateFlag)73     public void setStateOnUiThread(int stateFlag) {
74         if (Looper.myLooper() == Looper.getMainLooper()) {
75             setState(stateFlag);
76         } else {
77             postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> setState(stateFlag));
78         }
79     }
80 
81     /**
82      * Adds the provided state flags to the global state and executes any callbacks as a result.
83      */
setState(int stateFlag)84     public void setState(int stateFlag) {
85         if (DEBUG_STATES) {
86             Log.d(TAG, "[" + System.identityHashCode(this) + "] Adding "
87                     + convertToFlagNames(stateFlag) + " to " + convertToFlagNames(mState));
88         }
89         trackGestureEvents(stateFlag);
90         final int oldState = mState;
91         mState = mState | stateFlag;
92 
93         int count = mCallbacks.size();
94         for (int i = 0; i < count; i++) {
95             int state = mCallbacks.keyAt(i);
96 
97             if ((mState & state) == state) {
98                 LinkedList<Runnable> callbacks = mCallbacks.valueAt(i);
99                 while (!callbacks.isEmpty()) {
100                     callbacks.pollFirst().run();
101                 }
102             }
103         }
104         notifyStateChangeListeners(oldState);
105     }
106 
trackGestureEvents(int stateFlags)107     private void trackGestureEvents(int stateFlags) {
108         for (int index = 0; (stateFlags >> index) != 0; index++) {
109             if ((stateFlags & (1 << index)) == 0) {
110                 continue;
111             }
112             ActiveGestureErrorDetector.GestureEvent gestureEvent =
113                     mTrackedEventsMapper.getTrackedEventForState(1 << index);
114             if (gestureEvent == null) {
115                 continue;
116             }
117             if (gestureEvent.mLogEvent && gestureEvent.mTrackEvent) {
118                 ActiveGestureLog.INSTANCE.addLog(gestureEvent.name(), gestureEvent);
119             } else if (gestureEvent.mLogEvent) {
120                 ActiveGestureLog.INSTANCE.addLog(gestureEvent.name());
121             } else if (gestureEvent.mTrackEvent) {
122                 ActiveGestureLog.INSTANCE.trackEvent(gestureEvent);
123             }
124         }
125     }
126 
127     /**
128      * Adds the provided state flags to the global state and executes any change handlers
129      * as a result.
130      */
clearState(int stateFlag)131     public void clearState(int stateFlag) {
132         if (DEBUG_STATES) {
133             Log.d(TAG, "[" + System.identityHashCode(this) + "] Removing "
134                     + convertToFlagNames(stateFlag) + " from " + convertToFlagNames(mState));
135         }
136 
137         int oldState = mState;
138         mState = mState & ~stateFlag;
139         notifyStateChangeListeners(oldState);
140     }
141 
notifyStateChangeListeners(int oldState)142     private void notifyStateChangeListeners(int oldState) {
143         int count = mStateChangeListeners.size();
144         for (int i = 0; i < count; i++) {
145             int state = mStateChangeListeners.keyAt(i);
146             boolean wasOn = (state & oldState) == state;
147             boolean isOn = (state & mState) == state;
148 
149             if (wasOn != isOn) {
150                 ArrayList<Consumer<Boolean>> listeners = mStateChangeListeners.valueAt(i);
151                 for (Consumer<Boolean> listener : listeners) {
152                     listener.accept(isOn);
153                 }
154             }
155         }
156     }
157 
158     /**
159      * Sets a callback to be run when the provided states in the given {@param stateMask} is
160      * enabled. The callback is only run *once*, and if the states are already set at the time of
161      * this call then the callback will be made immediately.
162      */
runOnceAtState(int stateMask, Runnable callback)163     public void runOnceAtState(int stateMask, Runnable callback) {
164         if ((mState & stateMask) == stateMask) {
165             callback.run();
166         } else {
167             final LinkedList<Runnable> callbacks;
168             if (mCallbacks.indexOfKey(stateMask) >= 0) {
169                 callbacks = mCallbacks.get(stateMask);
170                 if (FeatureFlags.IS_STUDIO_BUILD && callbacks.contains(callback)) {
171                     throw new IllegalStateException("Existing callback for state found");
172                 }
173             } else {
174                 callbacks = new LinkedList<>();
175                 mCallbacks.put(stateMask, callbacks);
176             }
177             callbacks.add(callback);
178         }
179     }
180 
181     /**
182      * Adds a persistent listener to be called states in the given {@param stateMask} are enabled
183      * or disabled.
184      */
addChangeListener(int stateMask, Consumer<Boolean> listener)185     public void addChangeListener(int stateMask, Consumer<Boolean> listener) {
186         final ArrayList<Consumer<Boolean>> listeners;
187         if (mStateChangeListeners.indexOfKey(stateMask) >= 0) {
188             listeners = mStateChangeListeners.get(stateMask);
189         } else {
190             listeners = new ArrayList<>();
191             mStateChangeListeners.put(stateMask, listeners);
192         }
193         listeners.add(listener);
194     }
195 
getState()196     public int getState() {
197         return mState;
198     }
199 
hasStates(int stateMask)200     public boolean hasStates(int stateMask) {
201         return (mState & stateMask) == stateMask;
202     }
203 
convertToFlagNames(int flags)204     private String convertToFlagNames(int flags) {
205         StringJoiner joiner = new StringJoiner(", ", "[", " (" + flags + ")]");
206         for (int i = 0; i < mStateNames.length; i++) {
207             if ((flags & (1 << i)) != 0) {
208                 joiner.add(mStateNames[i]);
209             }
210         }
211         return joiner.toString();
212     }
213 
214     public interface TrackedEventsMapper {
getTrackedEventForState(int stateflag)215         @Nullable ActiveGestureErrorDetector.GestureEvent getTrackedEventForState(int stateflag);
216     }
217 }
218