1 /* 2 * Copyright (C) 2021 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 com.android.server.nearby.common.locator; 18 19 import android.annotation.Nullable; 20 import android.content.Context; 21 import android.content.ContextWrapper; 22 import android.util.Log; 23 24 import androidx.annotation.VisibleForTesting; 25 26 import java.util.ArrayList; 27 import java.util.HashMap; 28 import java.util.Map; 29 30 /** Collection of bindings that map service types to their respective implementation(s). */ 31 public class Locator { 32 private static final Object UNBOUND = new Object(); 33 private final Context mContext; 34 @Nullable 35 private Locator mParent; 36 private final String mTag; // For debugging 37 private final Map<Class<?>, Object> mBindings = new HashMap<>(); 38 private final ArrayList<Module> mModules = new ArrayList<>(); 39 40 /** Thrown upon attempt to bind an interface twice. */ 41 public static class DuplicateBindingException extends RuntimeException { DuplicateBindingException(String msg)42 DuplicateBindingException(String msg) { 43 super(msg); 44 } 45 } 46 47 /** Constructor with a null parent. */ Locator(Context context)48 public Locator(Context context) { 49 this(context, null); 50 } 51 52 /** 53 * Constructor. Supply a valid context and the Locator's parent. 54 * 55 * <p>To find a suitable parent you may want to use findLocator. 56 */ Locator(Context context, @Nullable Locator parent)57 public Locator(Context context, @Nullable Locator parent) { 58 this.mContext = context; 59 this.mParent = parent; 60 this.mTag = context.getClass().getName(); 61 } 62 63 /** Attaches the parent to the locator. */ attachParent(Locator parent)64 public void attachParent(Locator parent) { 65 this.mParent = parent; 66 } 67 68 /** Associates the specified type with the supplied instance. */ bind(Class<T> type, T instance)69 public <T extends Object> Locator bind(Class<T> type, T instance) { 70 bindKeyValue(type, instance); 71 return this; 72 } 73 74 /** For tests only. Disassociates the specified type from any instance. */ 75 @VisibleForTesting overrideBindingForTest(Class<T> type, T instance)76 public <T extends Object> Locator overrideBindingForTest(Class<T> type, T instance) { 77 mBindings.remove(type); 78 return bind(type, instance); 79 } 80 81 /** For tests only. Force Locator to return null when try to get an instance. */ 82 @VisibleForTesting removeBindingForTest(Class<T> type)83 public <T> Locator removeBindingForTest(Class<T> type) { 84 Locator locator = this; 85 do { 86 locator.mBindings.put(type, UNBOUND); 87 locator = locator.mParent; 88 } while (locator != null); 89 return this; 90 } 91 92 /** Binds a module. */ bind(Module module)93 public synchronized Locator bind(Module module) { 94 mModules.add(module); 95 return this; 96 } 97 98 /** 99 * Searches the chain of locators for a binding for the given type. 100 * 101 * @throws IllegalStateException if no binding is found. 102 */ get(Class<T> type)103 public <T> T get(Class<T> type) { 104 T instance = getOptional(type); 105 if (instance != null) { 106 return instance; 107 } 108 109 String errorMessage = getUnboundErrorMessage(type); 110 throw new IllegalStateException(errorMessage); 111 } 112 getUnboundErrorMessage(Class<?> type)113 private String getUnboundErrorMessage(Class<?> type) { 114 StringBuilder sb = new StringBuilder(); 115 sb.append("Unbound type: ").append(type.getName()).append("\n").append( 116 "Searched locators:\n"); 117 Locator locator = this; 118 while (true) { 119 sb.append(locator.mTag); 120 locator = locator.mParent; 121 if (locator == null) { 122 break; 123 } 124 sb.append(" ->\n"); 125 } 126 return sb.toString(); 127 } 128 129 /** 130 * Searches the chain of locators for a binding for the given type. Returns null if no locator 131 * was 132 * found. 133 */ 134 @Nullable getOptional(Class<T> type)135 public <T> T getOptional(Class<T> type) { 136 Locator locator = this; 137 do { 138 T instance = locator.getInstance(type); 139 if (instance != null) { 140 return instance; 141 } 142 locator = locator.mParent; 143 } while (locator != null); 144 return null; 145 } 146 bindKeyValue(Class<T> key, T value)147 private synchronized <T extends Object> void bindKeyValue(Class<T> key, T value) { 148 Object boundInstance = mBindings.get(key); 149 if (boundInstance != null) { 150 if (boundInstance == UNBOUND) { 151 Log.w(mTag, "Bind call too late - someone already tried to get: " + key); 152 } else { 153 throw new DuplicateBindingException("Duplicate binding: " + key); 154 } 155 } 156 mBindings.put(key, value); 157 } 158 159 // Suppress warning of cast from Object -> T 160 @SuppressWarnings("unchecked") 161 @Nullable getInstance(Class<T> type)162 private synchronized <T> T getInstance(Class<T> type) { 163 if (mContext == null) { 164 throw new IllegalStateException("Locator not initialized yet."); 165 } 166 167 T instance = (T) mBindings.get(type); 168 if (instance != null) { 169 return instance != UNBOUND ? instance : null; 170 } 171 172 // Ask modules to supply a binding 173 int moduleCount = mModules.size(); 174 for (int i = 0; i < moduleCount; i++) { 175 mModules.get(i).configure(mContext, type, this); 176 } 177 178 instance = (T) mBindings.get(type); 179 if (instance == null) { 180 mBindings.put(type, UNBOUND); 181 } 182 return instance; 183 } 184 185 /** 186 * Iterates over all bound objects and gives the modules a chance to clean up the objects they 187 * have created. 188 */ destroy()189 public synchronized void destroy() { 190 for (Class<?> type : mBindings.keySet()) { 191 Object instance = mBindings.get(type); 192 if (instance == UNBOUND) { 193 continue; 194 } 195 196 for (Module module : mModules) { 197 module.destroy(mContext, type, instance); 198 } 199 } 200 mBindings.clear(); 201 } 202 203 /** Returns true if there are no bindings. */ isEmpty()204 public boolean isEmpty() { 205 return mBindings.isEmpty(); 206 } 207 208 /** Returns the parent locator or null if no parent. */ 209 @Nullable getParent()210 public Locator getParent() { 211 return mParent; 212 } 213 214 /** 215 * Finds the first locator, then searches the chain of locators for a binding for the given 216 * type. 217 * 218 * @throws IllegalStateException if no binding is found. 219 */ get(Context context, Class<T> type)220 public static <T> T get(Context context, Class<T> type) { 221 Locator locator = findLocator(context); 222 if (locator == null) { 223 throw new IllegalStateException("No locator found in context " + context); 224 } 225 return locator.get(type); 226 } 227 228 /** 229 * Find the first locator from the context wrapper. 230 */ getFromContextWrapper(LocatorContextWrapper wrapper, Class<T> type)231 public static <T> T getFromContextWrapper(LocatorContextWrapper wrapper, Class<T> type) { 232 Locator locator = wrapper.getLocator(); 233 if (locator == null) { 234 throw new IllegalStateException("No locator found in context wrapper"); 235 } 236 return locator.get(type); 237 } 238 239 /** 240 * Finds the first locator, then searches the chain of locators for a binding for the given 241 * type. 242 * Returns null if no binding was found. 243 */ 244 @Nullable getOptional(Context context, Class<T> type)245 public static <T> T getOptional(Context context, Class<T> type) { 246 Locator locator = findLocator(context); 247 if (locator == null) { 248 return null; 249 } 250 return locator.getOptional(type); 251 } 252 253 /** Finds the first locator in the context hierarchy. */ 254 @Nullable findLocator(Context context)255 public static Locator findLocator(Context context) { 256 Context applicationContext = context.getApplicationContext(); 257 boolean applicationContextVisited = false; 258 259 Context searchContext = context; 260 do { 261 Locator locator = tryGetLocator(searchContext); 262 if (locator != null) { 263 return locator; 264 } 265 266 applicationContextVisited |= (searchContext == applicationContext); 267 268 if (searchContext instanceof ContextWrapper) { 269 searchContext = ((ContextWrapper) context).getBaseContext(); 270 271 if (searchContext == null) { 272 throw new IllegalStateException( 273 "Invalid ContextWrapper -- If this is a Robolectric test, " 274 + "have you called ActivityController.create()?"); 275 } 276 } else if (!applicationContextVisited) { 277 searchContext = applicationContext; 278 } else { 279 searchContext = null; 280 } 281 } while (searchContext != null); 282 283 return null; 284 } 285 286 @Nullable tryGetLocator(Object object)287 private static Locator tryGetLocator(Object object) { 288 if (object instanceof LocatorContext) { 289 Locator locator = ((LocatorContext) object).getLocator(); 290 if (locator == null) { 291 throw new IllegalStateException( 292 "LocatorContext must not return null Locator: " + object); 293 } 294 return locator; 295 } 296 return null; 297 } 298 } 299