Flutter Engine
The Flutter Engine
PlayStoreDeferredComponentManager.java
Go to the documentation of this file.
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package io.flutter.embedding.engine.deferredcomponents;
6
7import android.annotation.SuppressLint;
8import android.content.Context;
9import android.content.pm.ApplicationInfo;
10import android.content.pm.PackageManager;
11import android.content.pm.PackageManager.NameNotFoundException;
12import android.content.res.AssetManager;
13import android.os.Build;
14import android.os.Bundle;
15import android.util.SparseArray;
16import android.util.SparseIntArray;
17import androidx.annotation.NonNull;
18import androidx.annotation.Nullable;
19import com.google.android.play.core.splitinstall.SplitInstallException;
20import com.google.android.play.core.splitinstall.SplitInstallManager;
21import com.google.android.play.core.splitinstall.SplitInstallManagerFactory;
22import com.google.android.play.core.splitinstall.SplitInstallRequest;
23import com.google.android.play.core.splitinstall.SplitInstallSessionState;
24import com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener;
25import com.google.android.play.core.splitinstall.model.SplitInstallErrorCode;
26import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus;
27import io.flutter.Log;
28import io.flutter.embedding.engine.FlutterJNI;
29import io.flutter.embedding.engine.loader.ApplicationInfoLoader;
30import io.flutter.embedding.engine.loader.FlutterApplicationInfo;
31import io.flutter.embedding.engine.systemchannels.DeferredComponentChannel;
32import java.io.File;
33import java.util.ArrayList;
34import java.util.HashMap;
35import java.util.LinkedList;
36import java.util.List;
37import java.util.Map;
38import java.util.Queue;
39
40/**
41 * Flutter default implementation of DeferredComponentManager that downloads deferred component from
42 * the Google Play store as a dynamic feature module.
43 */
45 private static final String TAG = "PlayStoreDeferredComponentManager";
46
47 public static final String MAPPING_KEY =
48 DeferredComponentManager.class.getName() + ".loadingUnitMapping";
49
50 private @NonNull SplitInstallManager splitInstallManager;
51 private @Nullable FlutterJNI flutterJNI;
52 private @Nullable DeferredComponentChannel channel;
53 private @NonNull Context context;
54 private @NonNull FlutterApplicationInfo flutterApplicationInfo;
55 // Each request to install a feature module gets a session ID. These maps associate
56 // the session ID with the loading unit and component name that was requested.
57 private @NonNull SparseArray<String> sessionIdToName;
58 private @NonNull SparseIntArray sessionIdToLoadingUnitId;
59 private @NonNull SparseArray<String> sessionIdToState;
60 private @NonNull Map<String, Integer> nameToSessionId;
61
62 protected @NonNull SparseArray<String> loadingUnitIdToComponentNames;
63 protected @NonNull SparseArray<String> loadingUnitIdToSharedLibraryNames;
64
65 private FeatureInstallStateUpdatedListener listener;
66
67 private class FeatureInstallStateUpdatedListener implements SplitInstallStateUpdatedListener {
68 @SuppressLint("DefaultLocale")
69 public void onStateUpdate(@NonNull SplitInstallSessionState state) {
70 int sessionId = state.sessionId();
71 if (sessionIdToName.get(sessionId) != null) {
72 switch (state.status()) {
73 case SplitInstallSessionStatus.FAILED:
74 {
75 Log.e(
76 TAG,
77 String.format(
78 "Module \"%s\" (sessionId %d) install failed with: %s",
79 sessionIdToName.get(sessionId), sessionId, state.errorCode()));
80 flutterJNI.deferredComponentInstallFailure(
81 sessionIdToLoadingUnitId.get(sessionId),
82 "Module install failed with " + state.errorCode(),
83 true);
84 if (channel != null) {
85 channel.completeInstallError(
86 sessionIdToName.get(sessionId),
87 "Android Deferred Component failed to install.");
88 }
89 sessionIdToName.delete(sessionId);
90 sessionIdToLoadingUnitId.delete(sessionId);
91 sessionIdToState.put(sessionId, "failed");
92 break;
93 }
94 case SplitInstallSessionStatus.INSTALLED:
95 {
96 Log.d(
97 TAG,
98 String.format(
99 "Module \"%s\" (sessionId %d) install successfully.",
100 sessionIdToName.get(sessionId), sessionId));
101 loadAssets(sessionIdToLoadingUnitId.get(sessionId), sessionIdToName.get(sessionId));
102 // We only load Dart shared lib for the loading unit id requested. Other loading units
103 // (if present) in the deferred component are not loaded, but can be loaded by
104 // calling again with their loading unit id. If no valid loadingUnitId was included in
105 // the installation request such as for an asset only feature, then we can skip this.
106 if (sessionIdToLoadingUnitId.get(sessionId) > 0) {
108 sessionIdToLoadingUnitId.get(sessionId), sessionIdToName.get(sessionId));
109 }
110 if (channel != null) {
111 channel.completeInstallSuccess(sessionIdToName.get(sessionId));
112 }
113 sessionIdToName.delete(sessionId);
114 sessionIdToLoadingUnitId.delete(sessionId);
115 sessionIdToState.put(sessionId, "installed");
116 break;
117 }
118 case SplitInstallSessionStatus.CANCELED:
119 {
120 Log.d(
121 TAG,
122 String.format(
123 "Module \"%s\" (sessionId %d) install canceled.",
124 sessionIdToName.get(sessionId), sessionId));
125 if (channel != null) {
126 channel.completeInstallError(
127 sessionIdToName.get(sessionId),
128 "Android Deferred Component installation canceled.");
129 }
130 sessionIdToName.delete(sessionId);
131 sessionIdToLoadingUnitId.delete(sessionId);
132 sessionIdToState.put(sessionId, "cancelled");
133 break;
134 }
135 case SplitInstallSessionStatus.CANCELING:
136 {
137 Log.d(
138 TAG,
139 String.format(
140 "Module \"%s\" (sessionId %d) install canceling.",
141 sessionIdToName.get(sessionId), sessionId));
142 sessionIdToState.put(sessionId, "canceling");
143 break;
144 }
145 case SplitInstallSessionStatus.PENDING:
146 {
147 Log.d(
148 TAG,
149 String.format(
150 "Module \"%s\" (sessionId %d) install pending.",
151 sessionIdToName.get(sessionId), sessionId));
152 sessionIdToState.put(sessionId, "pending");
153 break;
154 }
155 case SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION:
156 {
157 Log.d(
158 TAG,
159 String.format(
160 "Module \"%s\" (sessionId %d) install requires user confirmation.",
161 sessionIdToName.get(sessionId), sessionId));
162 sessionIdToState.put(sessionId, "requiresUserConfirmation");
163 break;
164 }
165 case SplitInstallSessionStatus.DOWNLOADING:
166 {
167 Log.d(
168 TAG,
169 String.format(
170 "Module \"%s\" (sessionId %d) downloading.",
171 sessionIdToName.get(sessionId), sessionId));
172 sessionIdToState.put(sessionId, "downloading");
173 break;
174 }
175 case SplitInstallSessionStatus.DOWNLOADED:
176 {
177 Log.d(
178 TAG,
179 String.format(
180 "Module \"%s\" (sessionId %d) downloaded.",
181 sessionIdToName.get(sessionId), sessionId));
182 sessionIdToState.put(sessionId, "downloaded");
183 break;
184 }
185 case SplitInstallSessionStatus.INSTALLING:
186 {
187 Log.d(
188 TAG,
189 String.format(
190 "Module \"%s\" (sessionId %d) installing.",
191 sessionIdToName.get(sessionId), sessionId));
192 sessionIdToState.put(sessionId, "installing");
193 break;
194 }
195 default:
196 Log.d(TAG, "Unknown status: " + state.status());
197 }
198 }
199 }
200 }
201
203 @NonNull Context context, @Nullable FlutterJNI flutterJNI) {
204 this.context = context;
205 this.flutterJNI = flutterJNI;
206 this.flutterApplicationInfo = ApplicationInfoLoader.load(context);
207 splitInstallManager = SplitInstallManagerFactory.create(context);
208 listener = new FeatureInstallStateUpdatedListener();
209 splitInstallManager.registerListener(listener);
210 sessionIdToName = new SparseArray<>();
211 sessionIdToLoadingUnitId = new SparseIntArray();
212 sessionIdToState = new SparseArray<>();
213 nameToSessionId = new HashMap<>();
214
215 loadingUnitIdToComponentNames = new SparseArray<>();
216 loadingUnitIdToSharedLibraryNames = new SparseArray<>();
217 initLoadingUnitMappingToComponentNames();
218 }
219
220 public void setJNI(@NonNull FlutterJNI flutterJNI) {
221 this.flutterJNI = flutterJNI;
222 }
223
224 private boolean verifyJNI() {
225 if (flutterJNI == null) {
226 Log.e(
227 TAG,
228 "No FlutterJNI provided. `setJNI` must be called on the DeferredComponentManager before attempting to load dart libraries or invoking with platform channels.");
229 return false;
230 }
231 return true;
232 }
233
235 this.channel = channel;
236 }
237
238 @NonNull
239 private ApplicationInfo getApplicationInfo() {
240 try {
241 return context
242 .getPackageManager()
243 .getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
244 } catch (NameNotFoundException e) {
245 throw new RuntimeException(e);
246 }
247 }
248
249 // Obtain and parses the metadata string. An example encoded string is:
250 //
251 // "2:component2,3:component3,4:component1:libcomponent4.so,5:"
252 //
253 // Where loading unit 2 is included in component2, loading unit 3 is
254 // included in component3, and loading unit 4 is included in component1.
255 // An optional third parameter can be added to indicate the name of
256 // the shared library of the loading unit. Loading unit 5 maps to an empty
257 // string, indicating it is included in the base module and no dynamic
258 // feature modules need to be downloaded.
259 private void initLoadingUnitMappingToComponentNames() {
260 String mappingKey = DeferredComponentManager.class.getName() + ".loadingUnitMapping";
261 ApplicationInfo applicationInfo = getApplicationInfo();
262 if (applicationInfo != null) {
263 Bundle metaData = applicationInfo.metaData;
264 if (metaData != null) {
265 String rawMappingString = metaData.getString(MAPPING_KEY, null);
266 if (rawMappingString == null) {
267 Log.e(
268 TAG,
269 "No loading unit to dynamic feature module name found. Ensure '"
271 + "' is defined in the base module's AndroidManifest.");
272 return;
273 }
274 if (rawMappingString.equals("")) {
275 // Asset-only components, so no loading units to map.
276 return;
277 }
278 for (String entry : rawMappingString.split(",")) {
279 // Split with -1 param to include empty string following trailing ":"
280 String[] splitEntry = entry.split(":", -1);
281 int loadingUnitId = Integer.parseInt(splitEntry[0]);
282 loadingUnitIdToComponentNames.put(loadingUnitId, splitEntry[1]);
283 if (splitEntry.length > 2) {
284 loadingUnitIdToSharedLibraryNames.put(loadingUnitId, splitEntry[2]);
285 }
286 }
287 }
288 }
289 }
290
291 public void installDeferredComponent(int loadingUnitId, @Nullable String componentName) {
292 String resolvedComponentName =
293 componentName != null ? componentName : loadingUnitIdToComponentNames.get(loadingUnitId);
294 if (resolvedComponentName == null) {
295 Log.e(
296 TAG, "Deferred component name was null and could not be resolved from loading unit id.");
297 return;
298 }
299
300 // Handle a loading unit that is included in the base module that does not need download.
301 if (resolvedComponentName.equals("") && loadingUnitId > 0) {
302 // No need to load assets as base assets are already loaded.
303 loadDartLibrary(loadingUnitId, resolvedComponentName);
304 return;
305 }
306
307 SplitInstallRequest request =
308 SplitInstallRequest.newBuilder().addModule(resolvedComponentName).build();
309
310 splitInstallManager
311 // Submits the request to install the module through the
312 // asynchronous startInstall() task. Your app needs to be
313 // in the foreground to submit the request.
314 .startInstall(request)
315 // Called when the install request is sent successfully. This is different than a successful
316 // install which is handled in FeatureInstallStateUpdatedListener.
317 .addOnSuccessListener(
318 sessionId -> {
319 sessionIdToName.put(sessionId, resolvedComponentName);
320 sessionIdToLoadingUnitId.put(sessionId, loadingUnitId);
321 if (nameToSessionId.containsKey(resolvedComponentName)) {
322 sessionIdToState.remove(nameToSessionId.get(resolvedComponentName));
323 }
324 nameToSessionId.put(resolvedComponentName, sessionId);
325 sessionIdToState.put(sessionId, "Requested");
326 })
327 .addOnFailureListener(
328 exception -> {
329 switch (((SplitInstallException) exception).getErrorCode()) {
330 case SplitInstallErrorCode.NETWORK_ERROR:
331 flutterJNI.deferredComponentInstallFailure(
332 loadingUnitId,
333 "Install of deferred component module \""
334 + componentName
335 + "\" failed with a network error",
336 true);
337 break;
338 case SplitInstallErrorCode.MODULE_UNAVAILABLE:
339 flutterJNI.deferredComponentInstallFailure(
340 loadingUnitId,
341 "Install of deferred component module \""
342 + componentName
343 + "\" failed as it is unavailable",
344 false);
345 break;
346 default:
347 flutterJNI.deferredComponentInstallFailure(
348 loadingUnitId,
349 String.format(
350 "Install of deferred component module \"%s\" failed with error %d: %s",
351 componentName,
352 ((SplitInstallException) exception).getErrorCode(),
353 ((SplitInstallException) exception).getMessage()),
354 false);
355 break;
356 }
357 });
358 }
359
360 @NonNull
362 int loadingUnitId, @Nullable String componentName) {
363 String resolvedComponentName =
364 componentName != null ? componentName : loadingUnitIdToComponentNames.get(loadingUnitId);
365 if (resolvedComponentName == null) {
366 Log.e(
367 TAG, "Deferred component name was null and could not be resolved from loading unit id.");
368 return "unknown";
369 }
370 if (!nameToSessionId.containsKey(resolvedComponentName)) {
371 if (splitInstallManager.getInstalledModules().contains(resolvedComponentName)) {
372 return "installedPendingLoad";
373 }
374 return "unknown";
375 }
376 int sessionId = nameToSessionId.get(resolvedComponentName);
377 return sessionIdToState.get(sessionId);
378 }
379
380 public void loadAssets(int loadingUnitId, @NonNull String componentName) {
381 if (!verifyJNI()) {
382 return;
383 }
384 // Since android deferred component asset manager is handled through
385 // context, neither parameter is used here. Assets are stored in
386 // the apk's `assets` directory allowing them to be accessed by
387 // Android's AssetManager directly.
388 try {
389 context = context.createPackageContext(context.getPackageName(), 0);
390
391 AssetManager assetManager = context.getAssets();
392 flutterJNI.updateJavaAssetManager(assetManager, flutterApplicationInfo.flutterAssetsDir);
393 } catch (NameNotFoundException e) {
394 throw new RuntimeException(e);
395 }
396 }
397
398 public void loadDartLibrary(int loadingUnitId, @NonNull String componentName) {
399 if (!verifyJNI()) {
400 return;
401 }
402 // Loading unit must be specified and valid to load a dart library.
403 if (loadingUnitId < 0) {
404 return;
405 }
406
407 String aotSharedLibraryName = loadingUnitIdToSharedLibraryNames.get(loadingUnitId);
408 if (aotSharedLibraryName == null) {
409 // If the filename is not specified, we use dart's loading unit naming convention.
410 aotSharedLibraryName =
411 flutterApplicationInfo.aotSharedLibraryName + "-" + loadingUnitId + ".part.so";
412 }
413
414 // Possible values: armeabi, armeabi-v7a, arm64-v8a, x86, x86_64, mips, mips64
415 String abi = Build.SUPPORTED_ABIS[0];
416 String pathAbi = abi.replace("-", "_"); // abis are represented with underscores in paths.
417
418 // TODO(garyq): Optimize this apk/file discovery process to use less i/o and be more
419 // performant and robust.
420
421 // Search directly in APKs first
422 List<String> apkPaths = new ArrayList<>();
423 // If not found in APKs, we check in extracted native libs for the lib directly.
424 List<String> soPaths = new ArrayList<>();
425
426 Queue<File> searchFiles = new LinkedList<>();
427 // Downloaded modules are stored here
428 searchFiles.add(context.getFilesDir());
429 // The initial installed apks are provided by `sourceDirs` in ApplicationInfo.
430 // The jniLibs we want are in the splits not the baseDir. These
431 // APKs are only searched as a fallback, as base libs generally do not need
432 // to be fully path referenced.
433 for (String path : context.getApplicationInfo().splitSourceDirs) {
434 searchFiles.add(new File(path));
435 }
436
437 while (!searchFiles.isEmpty()) {
438 File file = searchFiles.remove();
439 if (file != null && file.isDirectory() && file.listFiles() != null) {
440 for (File f : file.listFiles()) {
441 searchFiles.add(f);
442 }
443 continue;
444 }
445 String name = file.getName();
446 // Special case for "split_config" since android base module non-master apks are
447 // initially installed with the "split_config" prefix/name.
448 if (name.endsWith(".apk")
449 && (name.startsWith(componentName) || name.startsWith("split_config"))
450 && name.contains(pathAbi)) {
451 apkPaths.add(file.getAbsolutePath());
452 continue;
453 }
454 if (name.equals(aotSharedLibraryName)) {
455 soPaths.add(file.getAbsolutePath());
456 }
457 }
458
459 List<String> searchPaths = new ArrayList<>();
460
461 // Add the bare filename as the first search path. In some devices, the so
462 // file can be dlopen-ed with just the file name.
463 searchPaths.add(aotSharedLibraryName);
464
465 for (String path : apkPaths) {
466 searchPaths.add(path + "!lib/" + abi + "/" + aotSharedLibraryName);
467 }
468 for (String path : soPaths) {
469 searchPaths.add(path);
470 }
471
472 flutterJNI.loadDartDeferredLibrary(
473 loadingUnitId, searchPaths.toArray(new String[searchPaths.size()]));
474 }
475
476 public boolean uninstallDeferredComponent(int loadingUnitId, @Nullable String componentName) {
477 String resolvedComponentName =
478 componentName != null ? componentName : loadingUnitIdToComponentNames.get(loadingUnitId);
479 if (resolvedComponentName == null) {
480 Log.e(
481 TAG, "Deferred component name was null and could not be resolved from loading unit id.");
482 return false;
483 }
484 List<String> modulesToUninstall = new ArrayList<>();
485 modulesToUninstall.add(resolvedComponentName);
486 splitInstallManager.deferredUninstall(modulesToUninstall);
487 if (nameToSessionId.get(resolvedComponentName) != null) {
488 sessionIdToState.delete(nameToSessionId.get(resolvedComponentName));
489 }
490 return true;
491 }
492
493 public void destroy() {
494 splitInstallManager.unregisterListener(listener);
495 channel = null;
496 flutterJNI = null;
497 }
498}
static void d(@NonNull String tag, @NonNull String message)
Definition: Log.java:64
static void e(@NonNull String tag, @NonNull String message)
Definition: Log.java:84
PlayStoreDeferredComponentManager( @NonNull Context context, @Nullable FlutterJNI flutterJNI)
static FlutterApplicationInfo load(@NonNull Context applicationContext)
AtkStateType state
def Build(configs, env, options)
Definition: build.py:232
DEF_SWITCHES_START aot vmservice shared library Name of the *so containing AOT compiled Dart assets for launching the service isolate vm snapshot The VM snapshot data that will be memory mapped as read only SnapshotAssetPath must be present isolate snapshot The isolate snapshot data that will be memory mapped as read only SnapshotAssetPath must be present cache dir path
Definition: switches.h:57
DEF_SWITCHES_START aot vmservice shared library name
Definition: switches.h:32
#define TAG()