1 /*
2  * Copyright 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 
17 package androidx.recyclerview.selection;
18 
19 import static androidx.annotation.RestrictTo.Scope.LIBRARY;
20 import static androidx.core.util.Preconditions.checkArgument;
21 import static androidx.core.util.Preconditions.checkState;
22 import static androidx.recyclerview.selection.Shared.DEBUG;
23 
24 import android.util.Log;
25 
26 import androidx.annotation.MainThread;
27 import androidx.annotation.RestrictTo;
28 
29 import org.jspecify.annotations.NonNull;
30 
31 import java.util.ArrayList;
32 import java.util.List;
33 
34 /**
35  * OperationMonitor provides a mechanism to coordinate application
36  * logic with ongoing user selection activities (such as active band selection
37  * and active gesture selection).
38  *
39  * <p>
40  * The host {@link android.app.Activity} or {@link android.app.Fragment} should avoid changing
41  * {@link androidx.recyclerview.widget.RecyclerView.Adapter Adapter} data while there
42  * are active selection operations, as this can result in a poor user experience.
43  *
44  * <p>
45  * To know when an operation is active listen to changes using an {@link OnChangeListener}.
46  */
47 public final class OperationMonitor {
48 
49     private static final String TAG = "OperationMonitor";
50 
51     private final List<OnChangeListener> mListeners = new ArrayList<>();
52 
53     // Ideally OperationMonitor would implement Resettable
54     // directly, but Metalava couldn't understand that
55     // `OperationMonitor` was public API while `Resettable` was
56     // not. This is our clever workaround :)
57     private final Resettable mResettable = new Resettable() {
58 
59         @Override
60         public boolean isResetRequired() {
61             return OperationMonitor.this.isResetRequired();
62         }
63 
64         @Override
65         public void reset() {
66             OperationMonitor.this.reset();
67         }
68     };
69 
70     private int mNumOps = 0;
71 
72     private final Object mLock = new Object();
73 
74     @MainThread
start()75     void start() {
76         synchronized (mLock) {
77             mNumOps++;
78 
79             if (mNumOps == 1) {
80                 notifyStateChanged();
81             }
82 
83             if (DEBUG) Log.v(TAG, "Incremented content lock count to " + mNumOps + ".");
84         }
85     }
86 
87     @MainThread
stop()88     void stop() {
89         synchronized (mLock) {
90             if (mNumOps == 0) {
91                 if (DEBUG) Log.w(TAG, "Stop called whith opt count of 0.");
92                 return;
93             }
94 
95             mNumOps--;
96             if (DEBUG) Log.v(TAG, "Decremented content lock count to " + mNumOps + ".");
97 
98             if (mNumOps == 0) {
99                 notifyStateChanged();
100             }
101         }
102     }
103 
104     @RestrictTo(LIBRARY)
105     @MainThread
reset()106     void reset() {
107         synchronized (mLock) {
108             if (DEBUG) Log.d(TAG, "Received reset request.");
109             if (mNumOps > 0) {
110                 Log.w(TAG, "Resetting OperationMonitor with " + mNumOps + " active operations.");
111             }
112             mNumOps = 0;
113             notifyStateChanged();
114         }
115     }
116 
117     @RestrictTo(LIBRARY)
isResetRequired()118     boolean isResetRequired() {
119         synchronized (mLock) {
120             return isStarted();
121         }
122     }
123 
124     /**
125      * @return true if there are any running operations.
126      */
isStarted()127     public boolean isStarted() {
128         synchronized (mLock) {
129             return mNumOps > 0;
130         }
131     }
132 
133     /**
134      * Registers supplied listener to be notified when operation status changes.
135      */
addListener(@onNull OnChangeListener listener)136     public void addListener(@NonNull OnChangeListener listener) {
137         checkArgument(listener != null);
138         mListeners.add(listener);
139     }
140 
141     /**
142      * Unregisters listener for further notifications.
143      */
removeListener(@onNull OnChangeListener listener)144     public void removeListener(@NonNull OnChangeListener listener) {
145         checkArgument(listener != null);
146         mListeners.remove(listener);
147     }
148 
149     /**
150      * Allows other selection code to perform a precondition check asserting the state is locked.
151      */
checkStarted(boolean started)152     void checkStarted(boolean started) {
153         if (started) {
154             checkState(mNumOps > 0);
155         } else {
156             checkState(mNumOps == 0);
157         }
158     }
159 
notifyStateChanged()160     private void notifyStateChanged() {
161         for (OnChangeListener l : mListeners) {
162             l.onChanged();
163         }
164     }
165 
166     /**
167      * Work around b/139109223.
168      */
169     @RestrictTo(LIBRARY)
asResettable()170     @NonNull Resettable asResettable() {
171         return mResettable;
172     }
173 
174     /**
175      * Listen to changes in operation status. Authors should avoid
176      * changing the Adapter model while there are active operations.
177      */
178     public interface OnChangeListener {
179 
180         /**
181          * Called when operation status changes. Call {@link OperationMonitor#isStarted()}
182          * to determine the current status.
183          */
onChanged()184         void onChanged();
185     }
186 }
187