blob: 2003335b7516b74b87d360209fad03b1b9bee3e6 [file] [log] [blame]
/*
* Copyright (C) 2021 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 com.android.server.nearby.common.locator;
import android.annotation.Nullable;
import android.content.Context;
import android.content.ContextWrapper;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
/** Collection of bindings that map service types to their respective implementation(s). */
public class Locator {
private static final Object UNBOUND = new Object();
private final Context mContext;
@Nullable
private Locator mParent;
private final String mTag; // For debugging
private final Map<Class<?>, Object> mBindings = new HashMap<>();
private final ArrayList<Module> mModules = new ArrayList<>();
/** Thrown upon attempt to bind an interface twice. */
public static class DuplicateBindingException extends RuntimeException {
DuplicateBindingException(String msg) {
super(msg);
}
}
/** Constructor with a null parent. */
public Locator(Context context) {
this(context, null);
}
/**
* Constructor. Supply a valid context and the Locator's parent.
*
* <p>To find a suitable parent you may want to use findLocator.
*/
public Locator(Context context, @Nullable Locator parent) {
this.mContext = context;
this.mParent = parent;
this.mTag = context.getClass().getName();
}
/** Attaches the parent to the locator. */
public void attachParent(Locator parent) {
this.mParent = parent;
}
/** Associates the specified type with the supplied instance. */
public <T extends Object> Locator bind(Class<T> type, T instance) {
bindKeyValue(type, instance);
return this;
}
/** For tests only. Disassociates the specified type from any instance. */
@VisibleForTesting
public <T extends Object> Locator overrideBindingForTest(Class<T> type, T instance) {
mBindings.remove(type);
return bind(type, instance);
}
/** For tests only. Force Locator to return null when try to get an instance. */
@VisibleForTesting
public <T> Locator removeBindingForTest(Class<T> type) {
Locator locator = this;
do {
locator.mBindings.put(type, UNBOUND);
locator = locator.mParent;
} while (locator != null);
return this;
}
/** Binds a module. */
public synchronized Locator bind(Module module) {
mModules.add(module);
return this;
}
/**
* Searches the chain of locators for a binding for the given type.
*
* @throws IllegalStateException if no binding is found.
*/
public <T> T get(Class<T> type) {
T instance = getOptional(type);
if (instance != null) {
return instance;
}
String errorMessage = getUnboundErrorMessage(type);
throw new IllegalStateException(errorMessage);
}
@VisibleForTesting
String getUnboundErrorMessage(Class<?> type) {
StringBuilder sb = new StringBuilder();
sb.append("Unbound type: ").append(type.getName()).append("\n").append(
"Searched locators:\n");
Locator locator = this;
while (true) {
sb.append(locator.mTag);
locator = locator.mParent;
if (locator == null) {
break;
}
sb.append(" ->\n");
}
return sb.toString();
}
/**
* Searches the chain of locators for a binding for the given type. Returns null if no locator
* was
* found.
*/
@Nullable
public <T> T getOptional(Class<T> type) {
Locator locator = this;
do {
T instance = locator.getInstance(type);
if (instance != null) {
return instance;
}
locator = locator.mParent;
} while (locator != null);
return null;
}
private synchronized <T extends Object> void bindKeyValue(Class<T> key, T value) {
Object boundInstance = mBindings.get(key);
if (boundInstance != null) {
if (boundInstance == UNBOUND) {
Log.w(mTag, "Bind call too late - someone already tried to get: " + key);
} else {
throw new DuplicateBindingException("Duplicate binding: " + key);
}
}
mBindings.put(key, value);
}
// Suppress warning of cast from Object -> T
@SuppressWarnings("unchecked")
@Nullable
private synchronized <T> T getInstance(Class<T> type) {
if (mContext == null) {
throw new IllegalStateException("Locator not initialized yet.");
}
T instance = (T) mBindings.get(type);
if (instance != null) {
return instance != UNBOUND ? instance : null;
}
// Ask modules to supply a binding
int moduleCount = mModules.size();
for (int i = 0; i < moduleCount; i++) {
mModules.get(i).configure(mContext, type, this);
}
instance = (T) mBindings.get(type);
if (instance == null) {
mBindings.put(type, UNBOUND);
}
return instance;
}
/**
* Iterates over all bound objects and gives the modules a chance to clean up the objects they
* have created.
*/
public synchronized void destroy() {
for (Class<?> type : mBindings.keySet()) {
Object instance = mBindings.get(type);
if (instance == UNBOUND) {
continue;
}
for (Module module : mModules) {
module.destroy(mContext, type, instance);
}
}
mBindings.clear();
}
/** Returns true if there are no bindings. */
public boolean isEmpty() {
return mBindings.isEmpty();
}
/** Returns the parent locator or null if no parent. */
@Nullable
public Locator getParent() {
return mParent;
}
/**
* Finds the first locator, then searches the chain of locators for a binding for the given
* type.
*
* @throws IllegalStateException if no binding is found.
*/
public static <T> T get(Context context, Class<T> type) {
Locator locator = findLocator(context);
if (locator == null) {
throw new IllegalStateException("No locator found in context " + context);
}
return locator.get(type);
}
/**
* Find the first locator from the context wrapper.
*/
public static <T> T getFromContextWrapper(LocatorContextWrapper wrapper, Class<T> type) {
Locator locator = wrapper.getLocator();
if (locator == null) {
throw new IllegalStateException("No locator found in context wrapper");
}
return locator.get(type);
}
/**
* Finds the first locator, then searches the chain of locators for a binding for the given
* type.
* Returns null if no binding was found.
*/
@Nullable
public static <T> T getOptional(Context context, Class<T> type) {
Locator locator = findLocator(context);
if (locator == null) {
return null;
}
return locator.getOptional(type);
}
/** Finds the first locator in the context hierarchy. */
@Nullable
public static Locator findLocator(Context context) {
Context applicationContext = context.getApplicationContext();
boolean applicationContextVisited = false;
Context searchContext = context;
do {
Locator locator = tryGetLocator(searchContext);
if (locator != null) {
return locator;
}
applicationContextVisited |= (searchContext == applicationContext);
if (searchContext instanceof ContextWrapper) {
searchContext = ((ContextWrapper) context).getBaseContext();
if (searchContext == null) {
throw new IllegalStateException(
"Invalid ContextWrapper -- If this is a Robolectric test, "
+ "have you called ActivityController.create()?");
}
} else if (!applicationContextVisited) {
searchContext = applicationContext;
} else {
searchContext = null;
}
} while (searchContext != null);
return null;
}
@Nullable
private static Locator tryGetLocator(Object object) {
if (object instanceof LocatorContext) {
Locator locator = ((LocatorContext) object).getLocator();
if (locator == null) {
throw new IllegalStateException(
"LocatorContext must not return null Locator: " + object);
}
return locator;
}
return null;
}
}