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