1 /*
2  * Copyright (C) 2023 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.providers.media.photopicker.sync;
18 
19 import android.annotation.IntDef;
20 import android.util.Log;
21 
22 import androidx.annotation.NonNull;
23 
24 import com.android.internal.annotations.VisibleForTesting;
25 import com.android.providers.media.photopicker.util.exceptions.UnableToAcquireLockException;
26 
27 import java.lang.annotation.Retention;
28 import java.lang.annotation.RetentionPolicy;
29 import java.util.concurrent.TimeUnit;
30 import java.util.concurrent.locks.ReentrantLock;
31 
32 /**
33  * Manages Java locks acquired during the sync process to ensure that the cloud sync is thread safe.
34  */
35 public class PickerSyncLockManager {
36     private static final String TAG = PickerSyncLockManager.class.getSimpleName();
37     private static final Integer LOCK_ACQUIRE_TIMEOUT_MINS = 4;
38     private static final TimeUnit LOCK_ACQUIRE_TIMEOUT_UNIT = TimeUnit.MINUTES;
39 
40     @IntDef(value = {CLOUD_SYNC_LOCK, CLOUD_ALBUM_SYNC_LOCK, CLOUD_PROVIDER_LOCK, DB_CLOUD_LOCK})
41     @Retention(RetentionPolicy.SOURCE)
42     public @interface LockType {}
43     public static final int CLOUD_SYNC_LOCK = 0;
44     public static final int CLOUD_ALBUM_SYNC_LOCK = 1;
45     public static final int CLOUD_PROVIDER_LOCK = 2;
46     public static final int DB_CLOUD_LOCK = 3;
47 
48     private final CloseableReentrantLock mCloudSyncLock =
49             new CloseableReentrantLock("CLOUD_SYNC_LOCK");
50     private final CloseableReentrantLock mCloudAlbumSyncLock =
51             new CloseableReentrantLock("CLOUD_ALBUM_SYNC_LOCK");
52     private final CloseableReentrantLock mCloudProviderLock =
53             new CloseableReentrantLock("CLOUD_PROVIDER_LOCK");
54     private final CloseableReentrantLock mDbCloudLock =
55             new CloseableReentrantLock("DB_CLOUD_LOCK");
56 
57     /**
58      * Try to acquire lock with a default timeout after running some validations.
59      */
tryLock(@ockType int lockType)60     public CloseableReentrantLock tryLock(@LockType int lockType)
61             throws UnableToAcquireLockException {
62         return tryLock(lockType, LOCK_ACQUIRE_TIMEOUT_MINS, LOCK_ACQUIRE_TIMEOUT_UNIT);
63     }
64 
65     /**
66      * Try to acquire lock with the provided timeout after running some validations.
67      */
tryLock(@ockType int lockType, long timeout, TimeUnit unit)68     public CloseableReentrantLock tryLock(@LockType int lockType, long timeout, TimeUnit unit)
69             throws UnableToAcquireLockException {
70         return tryLock(getLock(lockType), timeout, unit);
71     }
72 
73     /**
74      * Try to acquire the given lock with the provided timeout after running some validations.
75      */
76     @VisibleForTesting
tryLock(@onNull CloseableReentrantLock lock, long timeout, TimeUnit unit)77     public CloseableReentrantLock tryLock(@NonNull CloseableReentrantLock lock,
78             long timeout, TimeUnit unit) throws UnableToAcquireLockException {
79         Log.d(TAG, "Trying to acquire lock " + lock + " with timeout.");
80         validateLockOrder(lock);
81         return lock.lockWithTimeout(timeout, unit);
82     }
83 
84     /**
85      * Try to acquire the lock after running some validations.
86      */
lock(@ockType int lockType)87     public CloseableReentrantLock lock(@LockType int lockType) {
88         final CloseableReentrantLock reentrantLock = getLock(lockType);
89         Log.d(TAG, "Trying to acquire lock " + reentrantLock);
90         validateLockOrder(reentrantLock);
91         reentrantLock.lock();
92         return reentrantLock;
93     }
94 
95     /**
96      * Return the {@link CloseableReentrantLock} corresponding to the given {@link LockType}.
97      * Throws a {@link RuntimeException} if the lock is not recognized.
98      */
99     @VisibleForTesting
getLock(@ockType int lockType)100     public CloseableReentrantLock getLock(@LockType int lockType) {
101         switch (lockType) {
102             case CLOUD_SYNC_LOCK:
103                 return mCloudSyncLock;
104             case CLOUD_ALBUM_SYNC_LOCK:
105                 return mCloudAlbumSyncLock;
106             case CLOUD_PROVIDER_LOCK:
107                 return mCloudProviderLock;
108             case DB_CLOUD_LOCK:
109                 return mDbCloudLock;
110             default:
111                 throw new RuntimeException("Unrecognizable lock type " + lockType);
112         }
113     }
114 
validateLockOrder(@onNull ReentrantLock lockToBeAcquired)115     private void validateLockOrder(@NonNull ReentrantLock lockToBeAcquired) {
116         if (lockToBeAcquired.equals(mCloudSyncLock)) {
117             validateLockOrder(lockToBeAcquired, mCloudAlbumSyncLock);
118             validateLockOrder(lockToBeAcquired, mCloudProviderLock);
119             validateLockOrder(lockToBeAcquired, mDbCloudLock);
120         } else if (lockToBeAcquired.equals(mCloudAlbumSyncLock)) {
121             validateLockOrder(lockToBeAcquired, mCloudSyncLock);
122             validateLockOrder(lockToBeAcquired, mCloudProviderLock);
123             validateLockOrder(lockToBeAcquired, mDbCloudLock);
124         } else if (lockToBeAcquired.equals(mCloudProviderLock)) {
125             validateLockOrder(lockToBeAcquired, mDbCloudLock);
126         }
127     }
128 
validateLockOrder(@onNull ReentrantLock lockToBeAcquired, @NonNull ReentrantLock lockThatShouldNotBeHeld)129     private void validateLockOrder(@NonNull ReentrantLock lockToBeAcquired,
130             @NonNull ReentrantLock lockThatShouldNotBeHeld) {
131         if (lockThatShouldNotBeHeld.isHeldByCurrentThread()) {
132             Log.e(TAG, String.format("Lock {%s} should not be held before acquiring lock {%s}"
133                             + " This could lead to a deadlock.",
134                     lockThatShouldNotBeHeld, lockToBeAcquired));
135         }
136     }
137 }
138