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>&lt;meta-data&gt;</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