1 /*
2  * Copyright (C) 2019 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 android.content.res.loader;
18 
19 import android.annotation.NonNull;
20 import android.content.res.ApkAssets;
21 import android.content.res.Resources;
22 import android.util.ArrayMap;
23 import android.util.ArraySet;
24 
25 import com.android.internal.annotations.GuardedBy;
26 import com.android.internal.util.ArrayUtils;
27 
28 import java.lang.ref.WeakReference;
29 import java.util.Arrays;
30 import java.util.Collections;
31 import java.util.List;
32 
33 /**
34  * A container for supplying {@link ResourcesProvider ResourcesProvider(s)} to {@link Resources}
35  * objects.
36  *
37  * <p>{@link ResourcesLoader ResourcesLoader(s)} are added to Resources objects to supply
38  * additional resources and assets or modify the values of existing resources and assets. Multiple
39  * Resources objects can share the same ResourcesLoaders and ResourcesProviders. Changes to the list
40  * of {@link ResourcesProvider ResourcesProvider(s)} a loader contains propagates to all Resources
41  * objects that use the loader.
42  *
43  * <p>Loaders must be added to Resources objects in increasing precedence order. A loader will
44  * override the resources and assets of loaders added before itself.
45  *
46  * <p>Providers retrieved with {@link #getProviders()} are listed in increasing precedence order. A
47  * provider will override the resources and assets of providers listed before itself.
48  *
49  * <p>Modifying the list of providers a loader contains or the list of loaders a Resources object
50  * contains can cause lock contention with the UI thread. APIs that modify the lists of loaders or
51  * providers should only be used on the UI thread. Providers can be instantiated on any thread
52  * without causing lock contention.
53  */
54 public class ResourcesLoader {
55     private final Object mLock = new Object();
56 
57     @GuardedBy("mLock")
58     private ApkAssets[] mApkAssets;
59 
60     @GuardedBy("mLock")
61     private ResourcesProvider[] mPreviousProviders;
62 
63     @GuardedBy("mLock")
64     private ResourcesProvider[] mProviders;
65 
66     @GuardedBy("mLock")
67     private ArrayMap<WeakReference<Object>, UpdateCallbacks> mChangeCallbacks = new ArrayMap<>();
68 
69     /** @hide */
70     public interface UpdateCallbacks {
71 
72         /**
73          * Invoked when a {@link ResourcesLoader} has a {@link ResourcesProvider} added, removed,
74          * or reordered.
75          *
76          * @param loader the loader that was updated
77          */
onLoaderUpdated(@onNull ResourcesLoader loader)78         void onLoaderUpdated(@NonNull ResourcesLoader loader);
79     }
80 
81     /**
82      * Retrieves the list of providers loaded into this instance. Providers are listed in increasing
83      * precedence order. A provider will override the values of providers listed before itself.
84      */
85     @NonNull
getProviders()86     public List<ResourcesProvider> getProviders() {
87         synchronized (mLock) {
88             return mProviders == null ? Collections.emptyList() : Arrays.asList(mProviders);
89         }
90     }
91 
92     /**
93      * Appends a provider to the end of the provider list. If the provider is already present in the
94      * loader list, the list will not be modified.
95      *
96      * <p>This should only be called from the UI thread to avoid lock contention when propagating
97      * provider changes.
98      *
99      * @param resourcesProvider the provider to add
100      */
addProvider(@onNull ResourcesProvider resourcesProvider)101     public void addProvider(@NonNull ResourcesProvider resourcesProvider) {
102         synchronized (mLock) {
103             mProviders = ArrayUtils.appendElement(ResourcesProvider.class, mProviders,
104                     resourcesProvider);
105             notifyProvidersChangedLocked();
106         }
107     }
108 
109     /**
110      * Removes a provider from the provider list. If the provider is not present in the provider
111      * list, the list will not be modified.
112      *
113      * <p>This should only be called from the UI thread to avoid lock contention when propagating
114      * provider changes.
115      *
116      * @param resourcesProvider the provider to remove
117      */
removeProvider(@onNull ResourcesProvider resourcesProvider)118     public void removeProvider(@NonNull ResourcesProvider resourcesProvider) {
119         synchronized (mLock) {
120             mProviders = ArrayUtils.removeElement(ResourcesProvider.class, mProviders,
121                     resourcesProvider);
122             notifyProvidersChangedLocked();
123         }
124     }
125 
126     /**
127      * Sets the list of providers.
128      *
129      * <p>This should only be called from the UI thread to avoid lock contention when propagating
130      * provider changes.
131      *
132      * @param resourcesProviders the new providers
133      */
setProviders(@onNull List<ResourcesProvider> resourcesProviders)134     public void setProviders(@NonNull List<ResourcesProvider> resourcesProviders) {
135         synchronized (mLock) {
136             mProviders = resourcesProviders.toArray(new ResourcesProvider[0]);
137             notifyProvidersChangedLocked();
138         }
139     }
140 
141     /**
142      * Removes all {@link ResourcesProvider ResourcesProvider(s)}.
143      *
144      * <p>This should only be called from the UI thread to avoid lock contention when propagating
145      * provider changes.
146      */
clearProviders()147     public void clearProviders() {
148         synchronized (mLock) {
149             mProviders = null;
150             notifyProvidersChangedLocked();
151         }
152     }
153 
154     /**
155      * Retrieves the list of {@link ApkAssets} used by the providers.
156      *
157      * @hide
158      */
159     @NonNull
getApkAssets()160     public List<ApkAssets> getApkAssets() {
161         synchronized (mLock) {
162             if (mApkAssets == null) {
163                 return Collections.emptyList();
164             }
165             return Arrays.asList(mApkAssets);
166         }
167     }
168 
169     /**
170      * Registers a callback to be invoked when {@link ResourcesProvider ResourcesProvider(s)}
171      * change.
172      * @param instance the instance tied to the callback
173      * @param callbacks the callback to invoke
174      *
175      * @hide
176      */
registerOnProvidersChangedCallback(@onNull Object instance, @NonNull UpdateCallbacks callbacks)177     public void registerOnProvidersChangedCallback(@NonNull Object instance,
178             @NonNull UpdateCallbacks callbacks) {
179         synchronized (mLock) {
180             mChangeCallbacks.put(new WeakReference<>(instance), callbacks);
181         }
182     }
183 
184     /**
185      * Removes a previously registered callback.
186      * @param instance the instance tied to the callback
187      *
188      * @hide
189      */
unregisterOnProvidersChangedCallback(@onNull Object instance)190     public void unregisterOnProvidersChangedCallback(@NonNull Object instance) {
191         synchronized (mLock) {
192             for (int i = 0, n = mChangeCallbacks.size(); i < n; i++) {
193                 final WeakReference<Object> key = mChangeCallbacks.keyAt(i);
194                 if (instance == key.get()) {
195                     mChangeCallbacks.removeAt(i);
196                     return;
197                 }
198             }
199         }
200     }
201 
202     /** Returns whether the arrays contain the same provider instances in the same order. */
arrayEquals(ResourcesProvider[] a1, ResourcesProvider[] a2)203     private static boolean arrayEquals(ResourcesProvider[] a1, ResourcesProvider[] a2) {
204         if (a1 == a2) {
205             return true;
206         }
207 
208         if (a1 == null || a2 == null) {
209             return false;
210         }
211 
212         if (a1.length != a2.length) {
213             return false;
214         }
215 
216         // Check that the arrays contain the exact same instances in the same order. Providers do
217         // not have any form of equivalence checking of whether the contents of two providers have
218         // equivalent apk assets.
219         for (int i = 0, n = a1.length; i < n; i++) {
220             if (a1[i] != a2[i]) {
221                 return false;
222             }
223         }
224 
225         return true;
226     }
227 
228     /**
229      * Invokes registered callbacks when the list of {@link ResourcesProvider} instances this loader
230      * uses changes.
231      */
notifyProvidersChangedLocked()232     private void notifyProvidersChangedLocked() {
233         final ArraySet<UpdateCallbacks> uniqueCallbacks = new ArraySet<>();
234         if (arrayEquals(mPreviousProviders, mProviders)) {
235             return;
236         }
237 
238         if (mProviders == null || mProviders.length == 0) {
239             mApkAssets = null;
240         } else {
241             mApkAssets = new ApkAssets[mProviders.length];
242             for (int i = 0, n = mProviders.length; i < n; i++) {
243                 mProviders[i].incrementRefCount();
244                 mApkAssets[i] = mProviders[i].getApkAssets();
245             }
246         }
247 
248         // Decrement the ref count after incrementing the new provider ref count so providers
249         // present before and after this method do not drop to zero references.
250         if (mPreviousProviders != null) {
251             for (ResourcesProvider provider : mPreviousProviders) {
252                 provider.decrementRefCount();
253             }
254         }
255 
256         mPreviousProviders = mProviders;
257 
258         for (int i = mChangeCallbacks.size() - 1; i >= 0; i--) {
259             final WeakReference<Object> key = mChangeCallbacks.keyAt(i);
260             if (key.get() == null) {
261                 mChangeCallbacks.removeAt(i);
262             } else {
263                 uniqueCallbacks.add(mChangeCallbacks.valueAt(i));
264             }
265         }
266 
267         for (int i = 0, n = uniqueCallbacks.size(); i < n; i++) {
268             uniqueCallbacks.valueAt(i).onLoaderUpdated(this);
269         }
270     }
271 }
272