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 113 @VisibleForTesting getUnboundErrorMessage(Class<?> type)114 String getUnboundErrorMessage(Class<?> type) { 115 StringBuilder sb = new StringBuilder(); 116 sb.append("Unbound type: ").append(type.getName()).append("\n").append( 117 "Searched locators:\n"); 118 Locator locator = this; 119 while (true) { 120 sb.append(locator.mTag); 121 locator = locator.mParent; 122 if (locator == null) { 123 break; 124 } 125 sb.append(" ->\n"); 126 } 127 return sb.toString(); 128 } 129 130 /** 131 * Searches the chain of locators for a binding for the given type. Returns null if no locator 132 * was 133 * found. 134 */ 135 @Nullable getOptional(Class<T> type)136 public <T> T getOptional(Class<T> type) { 137 Locator locator = this; 138 do { 139 T instance = locator.getInstance(type); 140 if (instance != null) { 141 return instance; 142 } 143 locator = locator.mParent; 144 } while (locator != null); 145 return null; 146 } 147 bindKeyValue(Class<T> key, T value)148 private synchronized <T extends Object> void bindKeyValue(Class<T> key, T value) { 149 Object boundInstance = mBindings.get(key); 150 if (boundInstance != null) { 151 if (boundInstance == UNBOUND) { 152 Log.w(mTag, "Bind call too late - someone already tried to get: " + key); 153 } else { 154 throw new DuplicateBindingException("Duplicate binding: " + key); 155 } 156 } 157 mBindings.put(key, value); 158 } 159 160 // Suppress warning of cast from Object -> T 161 @SuppressWarnings("unchecked") 162 @Nullable getInstance(Class<T> type)163 private synchronized <T> T getInstance(Class<T> type) { 164 if (mContext == null) { 165 throw new IllegalStateException("Locator not initialized yet."); 166 } 167 168 T instance = (T) mBindings.get(type); 169 if (instance != null) { 170 return instance != UNBOUND ? instance : null; 171 } 172 173 // Ask modules to supply a binding 174 int moduleCount = mModules.size(); 175 for (int i = 0; i < moduleCount; i++) { 176 mModules.get(i).configure(mContext, type, this); 177 } 178 179 instance = (T) mBindings.get(type); 180 if (instance == null) { 181 mBindings.put(type, UNBOUND); 182 } 183 return instance; 184 } 185 186 /** 187 * Iterates over all bound objects and gives the modules a chance to clean up the objects they 188 * have created. 189 */ destroy()190 public synchronized void destroy() { 191 for (Class<?> type : mBindings.keySet()) { 192 Object instance = mBindings.get(type); 193 if (instance == UNBOUND) { 194 continue; 195 } 196 197 for (Module module : mModules) { 198 module.destroy(mContext, type, instance); 199 } 200 } 201 mBindings.clear(); 202 } 203 204 /** Returns true if there are no bindings. */ isEmpty()205 public boolean isEmpty() { 206 return mBindings.isEmpty(); 207 } 208 209 /** Returns the parent locator or null if no parent. */ 210 @Nullable getParent()211 public Locator getParent() { 212 return mParent; 213 } 214 215 /** 216 * Finds the first locator, then searches the chain of locators for a binding for the given 217 * type. 218 * 219 * @throws IllegalStateException if no binding is found. 220 */ get(Context context, Class<T> type)221 public static <T> T get(Context context, Class<T> type) { 222 Locator locator = findLocator(context); 223 if (locator == null) { 224 throw new IllegalStateException("No locator found in context " + context); 225 } 226 return locator.get(type); 227 } 228 229 /** 230 * Find the first locator from the context wrapper. 231 */ getFromContextWrapper(LocatorContextWrapper wrapper, Class<T> type)232 public static <T> T getFromContextWrapper(LocatorContextWrapper wrapper, Class<T> type) { 233 Locator locator = wrapper.getLocator(); 234 if (locator == null) { 235 throw new IllegalStateException("No locator found in context wrapper"); 236 } 237 return locator.get(type); 238 } 239 240 /** 241 * Finds the first locator, then searches the chain of locators for a binding for the given 242 * type. 243 * Returns null if no binding was found. 244 */ 245 @Nullable getOptional(Context context, Class<T> type)246 public static <T> T getOptional(Context context, Class<T> type) { 247 Locator locator = findLocator(context); 248 if (locator == null) { 249 return null; 250 } 251 return locator.getOptional(type); 252 } 253 254 /** Finds the first locator in the context hierarchy. */ 255 @Nullable findLocator(Context context)256 public static Locator findLocator(Context context) { 257 Context applicationContext = context.getApplicationContext(); 258 boolean applicationContextVisited = false; 259 260 Context searchContext = context; 261 do { 262 Locator locator = tryGetLocator(searchContext); 263 if (locator != null) { 264 return locator; 265 } 266 267 applicationContextVisited |= (searchContext == applicationContext); 268 269 if (searchContext instanceof ContextWrapper) { 270 searchContext = ((ContextWrapper) context).getBaseContext(); 271 272 if (searchContext == null) { 273 throw new IllegalStateException( 274 "Invalid ContextWrapper -- If this is a Robolectric test, " 275 + "have you called ActivityController.create()?"); 276 } 277 } else if (!applicationContextVisited) { 278 searchContext = applicationContext; 279 } else { 280 searchContext = null; 281 } 282 } while (searchContext != null); 283 284 return null; 285 } 286 287 @Nullable tryGetLocator(Object object)288 private static Locator tryGetLocator(Object object) { 289 if (object instanceof LocatorContext) { 290 Locator locator = ((LocatorContext) object).getLocator(); 291 if (locator == null) { 292 throw new IllegalStateException( 293 "LocatorContext must not return null Locator: " + object); 294 } 295 return locator; 296 } 297 return null; 298 } 299 } 300