Commit 07cd83b5 authored by Tibor Goldschwendt's avatar Tibor Goldschwendt Committed by Commit Bot

[modules] Fake on-demand install flow for testing purposes

Fakes module install by copying a feature module on the device's
download folder into the directory where SplitCompat expects downloaded
modules. Then invokes SplitCompat to emulate the module. The fake
install mode is activated via a command line flag. The fake install mode
is useful for testing purposes where it is difficult to host the module
on Play.

+ Refactors install logic out of ModuleInstaller into the
  ModuleInstallerBackend.

+ Adds two backends - one for the existing install logic and one for the
  fake install logic described above.

Bug: 862702
Change-Id: I8f2deff34a9aa4f8c0d434429fe7895038be7464
Reviewed-on: https://chromium-review.googlesource.com/c/1308608
Commit-Queue: Tibor Goldschwendt <tiborg@chromium.org>
Reviewed-by: default avataragrieve <agrieve@chromium.org>
Cr-Commit-Position: refs/heads/master@{#604393}
parent f116db7c
......@@ -7,6 +7,9 @@ import("//build/config/android/rules.gni")
android_library("module_installer_java") {
java_files = [
"java/src/org/chromium/components/module_installer/ModuleInstaller.java",
"java/src/org/chromium/components/module_installer/ModuleInstallerBackend.java",
"java/src/org/chromium/components/module_installer/FakeModuleInstallerBackend.java",
"java/src/org/chromium/components/module_installer/PlayCoreModuleInstallerBackend.java",
]
deps = [
"//base:base_java",
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.module_installer;
import android.content.Context;
import com.google.android.play.core.splitcompat.SplitCompat;
import org.chromium.base.BuildInfo;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.task.AsyncTask;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
/**
* Backend that looks for module APKs on the device's disk instead of invoking the Play core API to
* install a feature module. This backend is used for testing purposes where the module is not
* uploaded to the Play server.
*/
class FakeModuleInstallerBackend extends ModuleInstallerBackend {
private static final String TAG = "FakeModInBackend";
private static final String MODULES_SRC_DIRECTORY_PATH = "/data/local/tmp/modules";
public FakeModuleInstallerBackend(OnFinishedListener listener) {
super(listener);
}
/**
* Copies {MODULES_SRC_DIRECTORY_PATH}/|moduleName|.apk to the folder where SplitCompat expects
* downloaded modules to be. Then calls SplitCompat to emulate the module.
*
* We copy the module so that this works on non-rooted devices. The path SplitCompat expects
* module to be is not accessible without rooting.
*/
@Override
public void install(String moduleName) {
ThreadUtils.assertOnUiThread();
new AsyncTask<Boolean>() {
@Override
protected Boolean doInBackground() {
return installInternal(moduleName);
}
@Override
protected void onPostExecute(Boolean success) {
onFinished(success, Arrays.asList(moduleName));
}
}
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
@Override
public void close() {
// No open resources. Nothing to be done here.
}
private boolean installInternal(String moduleName) {
Context context = ContextUtils.getApplicationContext();
int versionCode = BuildInfo.getInstance().versionCode;
// Path where SplitCompat looks for downloaded modules. May change in future releases of
// the Play Core SDK.
File dstModuleFile = joinPaths(context.getFilesDir().getPath(), "splitcompat",
Integer.toString(versionCode), "verified-splits", moduleName + ".apk");
File srcModuleFile = joinPaths(MODULES_SRC_DIRECTORY_PATH, moduleName + ".apk");
// NOTE: Need to give Chrome storage permission for this to work.
try (FileInputStream istream = new FileInputStream(srcModuleFile);
FileOutputStream ostream = new FileOutputStream(dstModuleFile)) {
ostream.getChannel().transferFrom(istream.getChannel(), 0, istream.getChannel().size());
} catch (RuntimeException | IOException e) {
Log.e(TAG, "Failed to install module", e);
return false;
}
// Tell SplitCompat to do a full emulation of the module. The method name is obfuscated
// since it is not part of the public API. We are using it here since this backend is for
// testing purposes only.
return SplitCompat.a(context);
}
private File joinPaths(String... paths) {
File result = new File("");
for (String path : paths) {
result = new File(result, path);
}
return result;
}
}
......@@ -5,30 +5,24 @@
package org.chromium.components.module_installer;
import com.google.android.play.core.splitcompat.SplitCompat;
import com.google.android.play.core.splitinstall.SplitInstallManager;
import com.google.android.play.core.splitinstall.SplitInstallManagerFactory;
import com.google.android.play.core.splitinstall.SplitInstallRequest;
import com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener;
import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus;
import org.chromium.base.CommandLine;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.StrictModeContext;
import org.chromium.base.ThreadUtils;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/** Installs Dynamic Feature Modules (DFMs). */
/** Installs dynamic feature modules (DFMs). */
public class ModuleInstaller {
private static final String TAG = "ModuleInstaller";
/** Command line switch for activating the fake backend. */
public static final String FAKE_FEATURE_MODULE_INSTALL = "fake-feature-module-install";
private static final Map<String, List<OnFinishedListener>> sModuleNameListenerMap =
new HashMap<>();
private static SplitInstallManager sManager;
private static SplitInstallStateUpdatedListener sUpdateListener;
private static ModuleInstallerBackend sBackend;
/** Listener for when a module install has finished. */
public interface OnFinishedListener {
......@@ -66,13 +60,7 @@ public class ModuleInstaller {
// Request is already running.
return;
}
SplitInstallRequest request =
SplitInstallRequest.newBuilder().addModule(moduleName).build();
getManager().startInstall(request).addOnFailureListener(exception -> {
Log.e(TAG, "Failed to request module '" + moduleName + "': " + exception);
onFinished(false, Arrays.asList(moduleName));
});
getBackend().install(moduleName);
}
private static void onFinished(boolean success, List<String> moduleNames) {
......@@ -89,34 +77,19 @@ public class ModuleInstaller {
}
if (sModuleNameListenerMap.isEmpty()) {
sManager.unregisterListener(sUpdateListener);
sUpdateListener = null;
sManager = null;
sBackend.close();
sBackend = null;
}
}
private static SplitInstallManager getManager() {
ThreadUtils.assertOnUiThread();
if (sManager == null) {
sManager = SplitInstallManagerFactory.create(ContextUtils.getApplicationContext());
sUpdateListener = (state) -> {
switch (state.status()) {
case SplitInstallSessionStatus.INSTALLED:
onFinished(true, state.moduleNames());
break;
case SplitInstallSessionStatus.CANCELED:
case SplitInstallSessionStatus.FAILED:
Log.e(TAG,
"Failed to install modules '" + state.moduleNames()
+ "': " + state.status());
onFinished(false, state.moduleNames());
break;
}
};
sManager.registerListener(sUpdateListener);
private static ModuleInstallerBackend getBackend() {
if (sBackend == null) {
ModuleInstallerBackend.OnFinishedListener listener = ModuleInstaller::onFinished;
sBackend = CommandLine.getInstance().hasSwitch(FAKE_FEATURE_MODULE_INSTALL)
? new FakeModuleInstallerBackend(listener)
: new PlayCoreModuleInstallerBackend(listener);
}
return sManager;
return sBackend;
}
private ModuleInstaller() {}
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.module_installer;
import org.chromium.base.ThreadUtils;
import java.util.List;
/** A backend for installing dynamic feature modules that contain the actual install logic. */
/* package */ abstract class ModuleInstallerBackend {
private final OnFinishedListener mListener;
/** Listener for when a module install has finished. */
interface OnFinishedListener {
/**
* Called when the module install has finished.
* @param success True if the install was successful.
* @param moduleNames Names of modules whose install is finished.
*/
void onFinished(boolean success, List<String> moduleNames);
}
public ModuleInstallerBackend(OnFinishedListener listener) {
ThreadUtils.assertOnUiThread();
mListener = listener;
}
/**
* Asynchronously installs module.
* @param moduleName Name of the module.
*/
public abstract void install(String moduleName);
/**
* Releases resources of this backend. Calling this method an install is in progress results in
* undefined behavior. Calling any other method on this backend after closing results in
* undefined behavior, too.
*/
public abstract void close();
/** To be called when module install has finished. */
protected void onFinished(boolean success, List<String> moduleNames) {
mListener.onFinished(success, moduleNames);
}
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.module_installer;
import com.google.android.play.core.splitinstall.SplitInstallManager;
import com.google.android.play.core.splitinstall.SplitInstallManagerFactory;
import com.google.android.play.core.splitinstall.SplitInstallRequest;
import com.google.android.play.core.splitinstall.SplitInstallSessionState;
import com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener;
import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.components.module_installer.ModuleInstallerBackend.OnFinishedListener;
import java.util.Arrays;
/**
* Backend that uses the Play Core SDK to download a module from Play and install it subsequently.
*/
/* package */ class PlayCoreModuleInstallerBackend
extends ModuleInstallerBackend implements SplitInstallStateUpdatedListener {
private static final String TAG = "PlayCoreModInBackend";
private final SplitInstallManager mManager;
private boolean mIsClosed;
public PlayCoreModuleInstallerBackend(OnFinishedListener listener) {
super(listener);
mManager = SplitInstallManagerFactory.create(ContextUtils.getApplicationContext());
mManager.registerListener(this);
}
@Override
public void install(String moduleName) {
assert !mIsClosed;
SplitInstallRequest request =
SplitInstallRequest.newBuilder().addModule(moduleName).build();
mManager.startInstall(request).addOnFailureListener(errorCode -> {
Log.e(TAG, "Failed to request module '%s': error code %s", moduleName, errorCode);
// If we reach this error condition |onStateUpdate| won't be called. Thus, call
// |onFinished| here.
onFinished(false, Arrays.asList(moduleName));
});
}
@Override
public void close() {
assert !mIsClosed;
mManager.unregisterListener(this);
mIsClosed = true;
}
@Override
public void onStateUpdate(SplitInstallSessionState state) {
assert !mIsClosed;
switch (state.status()) {
case SplitInstallSessionStatus.INSTALLED:
onFinished(true, state.moduleNames());
break;
case SplitInstallSessionStatus.CANCELED:
case SplitInstallSessionStatus.FAILED:
Log.e(TAG, "Failed to install modules '%s': error code %s", state.moduleNames(),
state.status());
onFinished(false, state.moduleNames());
break;
}
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment