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 com.android.launcher3.config.FeatureFlags;
26 
27 import java.util.ArrayList;
28 import java.util.LinkedList;
29 import java.util.StringJoiner;
30 import java.util.function.Consumer;
31 
32 /**
33  * Utility class to help manage multiple callbacks based on different states.
34  */
35 public class MultiStateCallback {
36 
37     private static final String TAG = "MultiStateCallback";
38     public static final boolean DEBUG_STATES = false;
39 
40     private final SparseArray<LinkedList<Runnable>> mCallbacks = new SparseArray<>();
41     private final SparseArray<ArrayList<Consumer<Boolean>>> mStateChangeListeners =
42             new SparseArray<>();
43 
44     private final String[] mStateNames;
45 
46     private int mState = 0;
47 
MultiStateCallback(String[] stateNames)48     public MultiStateCallback(String[] stateNames) {
49         mStateNames = DEBUG_STATES ? stateNames : null;
50     }
51 
52     /**
53      * Adds the provided state flags to the global state on the UI thread and executes any callbacks
54      * as a result.
55      */
setStateOnUiThread(int stateFlag)56     public void setStateOnUiThread(int stateFlag) {
57         if (Looper.myLooper() == Looper.getMainLooper()) {
58             setState(stateFlag);
59         } else {
60             postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> setState(stateFlag));
61         }
62     }
63 
64     /**
65      * Adds the provided state flags to the global state and executes any callbacks as a result.
66      */
setState(int stateFlag)67     public void setState(int stateFlag) {
68         if (DEBUG_STATES) {
69             Log.d(TAG, "[" + System.identityHashCode(this) + "] Adding "
70                     + convertToFlagNames(stateFlag) + " to " + convertToFlagNames(mState));
71         }
72 
73         final int oldState = mState;
74         mState = mState | stateFlag;
75 
76         int count = mCallbacks.size();
77         for (int i = 0; i < count; i++) {
78             int state = mCallbacks.keyAt(i);
79 
80             if ((mState & state) == state) {
81                 LinkedList<Runnable> callbacks = mCallbacks.valueAt(i);
82                 while (!callbacks.isEmpty()) {
83                     callbacks.pollFirst().run();
84                 }
85             }
86         }
87         notifyStateChangeListeners(oldState);
88     }
89 
90     /**
91      * Adds the provided state flags to the global state and executes any change handlers
92      * as a result.
93      */
clearState(int stateFlag)94     public void clearState(int stateFlag) {
95         if (DEBUG_STATES) {
96             Log.d(TAG, "[" + System.identityHashCode(this) + "] Removing "
97                     + convertToFlagNames(stateFlag) + " from " + convertToFlagNames(mState));
98         }
99 
100         int oldState = mState;
101         mState = mState & ~stateFlag;
102         notifyStateChangeListeners(oldState);
103     }
104 
notifyStateChangeListeners(int oldState)105     private void notifyStateChangeListeners(int oldState) {
106         int count = mStateChangeListeners.size();
107         for (int i = 0; i < count; i++) {
108             int state = mStateChangeListeners.keyAt(i);
109             boolean wasOn = (state & oldState) == state;
110             boolean isOn = (state & mState) == state;
111 
112             if (wasOn != isOn) {
113                 ArrayList<Consumer<Boolean>> listeners = mStateChangeListeners.valueAt(i);
114                 for (Consumer<Boolean> listener : listeners) {
115                     listener.accept(isOn);
116                 }
117             }
118         }
119     }
120 
121     /**
122      * Sets a callback to be run when the provided states in the given {@param stateMask} is
123      * enabled. The callback is only run *once*, and if the states are already set at the time of
124      * this call then the callback will be made immediately.
125      */
runOnceAtState(int stateMask, Runnable callback)126     public void runOnceAtState(int stateMask, Runnable callback) {
127         if ((mState & stateMask) == stateMask) {
128             callback.run();
129         } else {
130             final LinkedList<Runnable> callbacks;
131             if (mCallbacks.indexOfKey(stateMask) >= 0) {
132                 callbacks = mCallbacks.get(stateMask);
133                 if (FeatureFlags.IS_STUDIO_BUILD && callbacks.contains(callback)) {
134                     throw new IllegalStateException("Existing callback for state found");
135                 }
136             } else {
137                 callbacks = new LinkedList<>();
138                 mCallbacks.put(stateMask, callbacks);
139             }
140             callbacks.add(callback);
141         }
142     }
143 
144     /**
145      * Adds a persistent listener to be called states in the given {@param stateMask} are enabled
146      * or disabled.
147      */
addChangeListener(int stateMask, Consumer<Boolean> listener)148     public void addChangeListener(int stateMask, Consumer<Boolean> listener) {
149         final ArrayList<Consumer<Boolean>> listeners;
150         if (mStateChangeListeners.indexOfKey(stateMask) >= 0) {
151             listeners = mStateChangeListeners.get(stateMask);
152         } else {
153             listeners = new ArrayList<>();
154             mStateChangeListeners.put(stateMask, listeners);
155         }
156         listeners.add(listener);
157     }
158 
getState()159     public int getState() {
160         return mState;
161     }
162 
hasStates(int stateMask)163     public boolean hasStates(int stateMask) {
164         return (mState & stateMask) == stateMask;
165     }
166 
convertToFlagNames(int flags)167     private String convertToFlagNames(int flags) {
168         StringJoiner joiner = new StringJoiner(", ", "[", " (" + flags + ")]");
169         for (int i = 0; i < mStateNames.length; i++) {
170             if ((flags & (1 << i)) != 0) {
171                 joiner.add(mStateNames[i]);
172             }
173         }
174         return joiner.toString();
175     }
176 
177 }
178