blob: c3084003c3046ee1a24cf8dd2c891d8f4bbd193c [file] [log] [blame]
/*
* 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.
*
* <p>{@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.
*
* <p>Loaders must be added to Resources objects in increasing precedence order. A loader will
* override the resources and assets of loaders added before itself.
*
* <p>Providers retrieved with {@link #getProviders()} are listed in increasing precedence order. A
* provider will override the resources and assets of providers listed before itself.
*
* <p>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<WeakReference<Object>, 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<ResourcesProvider> 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.
*
* <p>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.
*
* <p>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.
*
* <p>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<ResourcesProvider> resourcesProviders) {
synchronized (mLock) {
mProviders = resourcesProviders.toArray(new ResourcesProvider[0]);
notifyProvidersChangedLocked();
}
}
/**
* Removes all {@link ResourcesProvider ResourcesProvider(s)}.
*
* <p>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<ApkAssets> 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<Object> 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<UpdateCallbacks> 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<Object> key = mChangeCallbacks.keyAt(i);
if (key.get() == 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);
}
}
}