Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
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
234 public void setDeferredComponentChannel(@NonNull DeferredComponentChannel channel) {
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}
void add(sk_sp< SkIDChangeListener > listener) SK_EXCLUDES(fMutex)
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)
AtkStateType state
const char * name
Definition fuchsia.cc:50
#define TAG()