1 /* 2 * Copyright 2020 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 androidx.startup; 18 19 import static android.content.pm.PackageManager.GET_META_DATA; 20 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.pm.PackageManager; 24 import android.content.pm.ProviderInfo; 25 import android.os.Bundle; 26 27 import androidx.tracing.Trace; 28 29 import org.jspecify.annotations.NonNull; 30 import org.jspecify.annotations.Nullable; 31 32 import java.util.HashMap; 33 import java.util.HashSet; 34 import java.util.List; 35 import java.util.Map; 36 import java.util.Set; 37 38 /** 39 * An {@link AppInitializer} can be used to initialize all discovered 40 * <code>ComponentInitializer</code>s. The discovery mechanism is via 41 * <code><meta-data></code> entries in the merged 42 * <code>AndroidManifest.xml</code>. 43 */ 44 @SuppressWarnings("WeakerAccess") 45 public final class AppInitializer { 46 47 // Tracing 48 private static final String SECTION_NAME = "Startup"; 49 50 /** 51 * The {@link AppInitializer} instance. 52 */ 53 private static volatile AppInitializer sInstance; 54 55 /** 56 * Guards app initialization. 57 */ 58 private static final Object sLock = new Object(); 59 60 final @NonNull Map<Class<?>, Object> mInitialized; 61 62 final @NonNull Set<Class<? extends Initializer<?>>> mDiscovered; 63 64 final @NonNull Context mContext; 65 66 /** 67 * Creates an instance of {@link AppInitializer} 68 * 69 * @param context The application context 70 */ AppInitializer(@onNull Context context)71 AppInitializer(@NonNull Context context) { 72 mContext = context.getApplicationContext(); 73 mDiscovered = new HashSet<>(); 74 mInitialized = new HashMap<>(); 75 } 76 77 /** 78 * @param context The Application {@link Context} 79 * @return The instance of {@link AppInitializer} after initialization. 80 */ 81 @SuppressWarnings("UnusedReturnValue") getInstance(@onNull Context context)82 public static @NonNull AppInitializer getInstance(@NonNull Context context) { 83 if (sInstance == null) { 84 synchronized (sLock) { 85 if (sInstance == null) { 86 sInstance = new AppInitializer(context); 87 } 88 } 89 } 90 return sInstance; 91 } 92 93 /** 94 * Sets an {@link AppInitializer} delegate. Useful in the context of testing. 95 * 96 * @param delegate The instance of {@link AppInitializer} to be used as a delegate. 97 */ setDelegate(@onNull AppInitializer delegate)98 static void setDelegate(@NonNull AppInitializer delegate) { 99 synchronized (sLock) { 100 sInstance = delegate; 101 } 102 } 103 104 /** 105 * Initializes a {@link Initializer} class type. 106 * 107 * @param component The {@link Class} of {@link Initializer} to initialize. 108 * @param <T> The instance type being initialized 109 * @return The initialized instance 110 */ 111 @SuppressWarnings("unused") initializeComponent(@onNull Class<? extends Initializer<T>> component)112 public <T> @NonNull T initializeComponent(@NonNull Class<? extends Initializer<T>> component) { 113 return doInitialize(component); 114 } 115 116 /** 117 * Returns <code>true</code> if the {@link Initializer} was eagerly initialized.. 118 * 119 * @param component The {@link Initializer} class to check 120 * @return <code>true</code> if the {@link Initializer} was eagerly initialized. 121 */ isEagerlyInitialized(@onNull Class<? extends Initializer<?>> component)122 public boolean isEagerlyInitialized(@NonNull Class<? extends Initializer<?>> component) { 123 // If discoverAndInitialize() was never called, then nothing was eagerly initialized. 124 return mDiscovered.contains(component); 125 } 126 127 @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) doInitialize(@onNull Class<? extends Initializer<?>> component)128 <T> @NonNull T doInitialize(@NonNull Class<? extends Initializer<?>> component) { 129 Object result; 130 synchronized (sLock) { 131 result = mInitialized.get(component); 132 if (result == null) { 133 result = doInitialize(component, new HashSet<Class<?>>()); 134 } 135 } 136 return (T) result; 137 } 138 139 @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) doInitialize( @onNull Class<? extends Initializer<?>> component, @NonNull Set<Class<?>> initializing)140 private <T> @NonNull T doInitialize( 141 @NonNull Class<? extends Initializer<?>> component, 142 @NonNull Set<Class<?>> initializing) { 143 boolean isTracingEnabled = Trace.isEnabled(); 144 try { 145 if (isTracingEnabled) { 146 // Use the simpleName here because section names would get too big otherwise. 147 Trace.beginSection(component.getSimpleName()); 148 } 149 if (initializing.contains(component)) { 150 String message = String.format( 151 "Cannot initialize %s. Cycle detected.", component.getName() 152 ); 153 throw new IllegalStateException(message); 154 } 155 Object result; 156 if (!mInitialized.containsKey(component)) { 157 initializing.add(component); 158 try { 159 Object instance = component.getDeclaredConstructor().newInstance(); 160 Initializer<?> initializer = (Initializer<?>) instance; 161 List<Class<? extends Initializer<?>>> dependencies = 162 initializer.dependencies(); 163 164 if (!dependencies.isEmpty()) { 165 for (Class<? extends Initializer<?>> clazz : dependencies) { 166 if (!mInitialized.containsKey(clazz)) { 167 doInitialize(clazz, initializing); 168 } 169 } 170 } 171 if (StartupLogger.DEBUG) { 172 StartupLogger.i(String.format("Initializing %s", component.getName())); 173 } 174 result = initializer.create(mContext); 175 if (StartupLogger.DEBUG) { 176 StartupLogger.i(String.format("Initialized %s", component.getName())); 177 } 178 initializing.remove(component); 179 mInitialized.put(component, result); 180 } catch (Throwable throwable) { 181 throw new StartupException(throwable); 182 } 183 } else { 184 result = mInitialized.get(component); 185 } 186 return (T) result; 187 } finally { 188 Trace.endSection(); 189 } 190 } 191 192 @SuppressWarnings("deprecation") discoverAndInitialize( @onNull Class<? extends InitializationProvider> initializationProvider)193 void discoverAndInitialize( 194 @NonNull Class<? extends InitializationProvider> initializationProvider) { 195 try { 196 Trace.beginSection(SECTION_NAME); 197 ComponentName provider = new ComponentName(mContext, initializationProvider); 198 ProviderInfo providerInfo = mContext.getPackageManager() 199 .getProviderInfo(provider, GET_META_DATA); 200 Bundle metadata = providerInfo.metaData; 201 discoverAndInitialize(metadata); 202 } catch (PackageManager.NameNotFoundException exception) { 203 throw new StartupException(exception); 204 } finally { 205 Trace.endSection(); 206 } 207 } 208 209 @SuppressWarnings("unchecked") discoverAndInitialize(@ullable Bundle metadata)210 void discoverAndInitialize(@Nullable Bundle metadata) { 211 String startup = mContext.getString(R.string.androidx_startup); 212 try { 213 if (metadata != null) { 214 Set<Class<?>> initializing = new HashSet<>(); 215 Set<String> keys = metadata.keySet(); 216 for (String key : keys) { 217 String value = metadata.getString(key, null); 218 if (startup.equals(value)) { 219 Class<?> clazz = Class.forName(key); 220 if (Initializer.class.isAssignableFrom(clazz)) { 221 Class<? extends Initializer<?>> component = 222 (Class<? extends Initializer<?>>) clazz; 223 mDiscovered.add(component); 224 if (StartupLogger.DEBUG) { 225 StartupLogger.i(String.format("Discovered %s", key)); 226 } 227 } 228 } 229 } 230 // Initialize only after discovery is complete. This way, the check for 231 // isEagerlyInitialized is correct. 232 for (Class<? extends Initializer<?>> component : mDiscovered) { 233 doInitialize(component, initializing); 234 } 235 } 236 } catch (ClassNotFoundException exception) { 237 throw new StartupException(exception); 238 } 239 } 240 } 241