/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.content.res.loader; import android.annotation.NonNull; import android.content.res.ApkAssets; import android.content.res.Resources; import android.util.ArrayMap; import android.util.ArraySet; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.ArrayUtils; import java.lang.ref.WeakReference; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * A container for supplying {@link ResourcesProvider ResourcesProvider(s)} to {@link Resources} * objects. * *

{@link ResourcesLoader ResourcesLoader(s)} are added to Resources objects to supply * additional resources and assets or modify the values of existing resources and assets. Multiple * Resources objects can share the same ResourcesLoaders and ResourcesProviders. Changes to the list * of {@link ResourcesProvider ResourcesProvider(s)} a loader contains propagates to all Resources * objects that use the loader. * *

Loaders must be added to Resources objects in increasing precedence order. A loader will * override the resources and assets of loaders added before itself. * *

Providers retrieved with {@link #getProviders()} are listed in increasing precedence order. A * provider will override the resources and assets of providers listed before itself. * *

Modifying the list of providers a loader contains or the list of loaders a Resources object * contains can cause lock contention with the UI thread. APIs that modify the lists of loaders or * providers should only be used on the UI thread. Providers can be instantiated on any thread * without causing lock contention. */ public class ResourcesLoader { private final Object mLock = new Object(); @GuardedBy("mLock") private ApkAssets[] mApkAssets; @GuardedBy("mLock") private ResourcesProvider[] mPreviousProviders; @GuardedBy("mLock") private ResourcesProvider[] mProviders; @GuardedBy("mLock") private ArrayMap, UpdateCallbacks> mChangeCallbacks = new ArrayMap<>(); /** @hide */ public interface UpdateCallbacks { /** * Invoked when a {@link ResourcesLoader} has a {@link ResourcesProvider} added, removed, * or reordered. * * @param loader the loader that was updated */ void onLoaderUpdated(@NonNull ResourcesLoader loader); } /** * Retrieves the list of providers loaded into this instance. Providers are listed in increasing * precedence order. A provider will override the values of providers listed before itself. */ @NonNull public List getProviders() { synchronized (mLock) { return mProviders == null ? Collections.emptyList() : Arrays.asList(mProviders); } } /** * Appends a provider to the end of the provider list. If the provider is already present in the * loader list, the list will not be modified. * *

This should only be called from the UI thread to avoid lock contention when propagating * provider changes. * * @param resourcesProvider the provider to add */ public void addProvider(@NonNull ResourcesProvider resourcesProvider) { synchronized (mLock) { mProviders = ArrayUtils.appendElement(ResourcesProvider.class, mProviders, resourcesProvider); notifyProvidersChangedLocked(); } } /** * Removes a provider from the provider list. If the provider is not present in the provider * list, the list will not be modified. * *

This should only be called from the UI thread to avoid lock contention when propagating * provider changes. * * @param resourcesProvider the provider to remove */ public void removeProvider(@NonNull ResourcesProvider resourcesProvider) { synchronized (mLock) { mProviders = ArrayUtils.removeElement(ResourcesProvider.class, mProviders, resourcesProvider); notifyProvidersChangedLocked(); } } /** * Sets the list of providers. * *

This should only be called from the UI thread to avoid lock contention when propagating * provider changes. * * @param resourcesProviders the new providers */ public void setProviders(@NonNull List resourcesProviders) { synchronized (mLock) { mProviders = resourcesProviders.toArray(new ResourcesProvider[0]); notifyProvidersChangedLocked(); } } /** * Removes all {@link ResourcesProvider ResourcesProvider(s)}. * *

This should only be called from the UI thread to avoid lock contention when propagating * provider changes. */ public void clearProviders() { synchronized (mLock) { mProviders = null; notifyProvidersChangedLocked(); } } /** * Retrieves the list of {@link ApkAssets} used by the providers. * * @hide */ @NonNull public List getApkAssets() { synchronized (mLock) { if (mApkAssets == null) { return Collections.emptyList(); } return Arrays.asList(mApkAssets); } } /** * Registers a callback to be invoked when {@link ResourcesProvider ResourcesProvider(s)} * change. * @param instance the instance tied to the callback * @param callbacks the callback to invoke * * @hide */ public void registerOnProvidersChangedCallback(@NonNull Object instance, @NonNull UpdateCallbacks callbacks) { synchronized (mLock) { mChangeCallbacks.put(new WeakReference<>(instance), callbacks); } } /** * Removes a previously registered callback. * @param instance the instance tied to the callback * * @hide */ public void unregisterOnProvidersChangedCallback(@NonNull Object instance) { synchronized (mLock) { for (int i = 0, n = mChangeCallbacks.size(); i < n; i++) { final WeakReference key = mChangeCallbacks.keyAt(i); if (instance == key.get()) { mChangeCallbacks.removeAt(i); return; } } } } /** Returns whether the arrays contain the same provider instances in the same order. */ private static boolean arrayEquals(ResourcesProvider[] a1, ResourcesProvider[] a2) { if (a1 == a2) { return true; } if (a1 == null || a2 == null) { return false; } if (a1.length != a2.length) { return false; } // Check that the arrays contain the exact same instances in the same order. Providers do // not have any form of equivalence checking of whether the contents of two providers have // equivalent apk assets. for (int i = 0, n = a1.length; i < n; i++) { if (a1[i] != a2[i]) { return false; } } return true; } /** * Invokes registered callbacks when the list of {@link ResourcesProvider} instances this loader * uses changes. */ private void notifyProvidersChangedLocked() { final ArraySet uniqueCallbacks = new ArraySet<>(); if (arrayEquals(mPreviousProviders, mProviders)) { return; } if (mProviders == null || mProviders.length == 0) { mApkAssets = null; } else { mApkAssets = new ApkAssets[mProviders.length]; for (int i = 0, n = mProviders.length; i < n; i++) { mProviders[i].incrementRefCount(); mApkAssets[i] = mProviders[i].getApkAssets(); } } // Decrement the ref count after incrementing the new provider ref count so providers // present before and after this method do not drop to zero references. if (mPreviousProviders != null) { for (ResourcesProvider provider : mPreviousProviders) { provider.decrementRefCount(); } } mPreviousProviders = mProviders; for (int i = mChangeCallbacks.size() - 1; i >= 0; i--) { final WeakReference key = mChangeCallbacks.keyAt(i); if (key.refersTo(null)) { mChangeCallbacks.removeAt(i); } else { uniqueCallbacks.add(mChangeCallbacks.valueAt(i)); } } for (int i = 0, n = uniqueCallbacks.size(); i < n; i++) { uniqueCallbacks.valueAt(i).onLoaderUpdated(this); } } }