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