SystemApi for dex module registration

PackageManager#registerDexModule() allows apps which can call
system apis to register a dex module with the Package Manager.

The PM may optimize the modules on the spot if needed. This is
particular useful for shared dex modules (e.g. chimera modules)
which are loaded in multiple processes.

Test:  adb  shell am instrument -e class
'android.content.pm.PackageManagerTests' -w
'com.android.frameworks.coretests/android.support.test.runner.AndroidJUnitRunner'
Bug: 37290820
Change-Id: I9ea8f937a76d2549a29e90a6c84c53c2e44a1ee4
This commit is contained in:
Calin Juravle
2017-04-19 19:56:21 -07:00
parent d600fce574
commit 3d2af7f72a
12 changed files with 415 additions and 5 deletions

View File

@@ -157,6 +157,7 @@ LOCAL_SRC_FILES += \
core/java/android/content/ISyncServiceAdapter.aidl \
core/java/android/content/ISyncStatusObserver.aidl \
core/java/android/content/om/IOverlayManager.aidl \
core/java/android/content/pm/IDexModuleRegisterCallback.aidl \
core/java/android/content/pm/ILauncherApps.aidl \
core/java/android/content/pm/IOnAppsChangedListener.aidl \
core/java/android/content/pm/IOnPermissionsChangeListener.aidl \

View File

@@ -11339,6 +11339,7 @@ package android.content.pm {
method public abstract java.util.List<android.content.pm.ResolveInfo> queryIntentContentProviders(android.content.Intent, int);
method public abstract java.util.List<android.content.pm.ResolveInfo> queryIntentServices(android.content.Intent, int);
method public abstract java.util.List<android.content.pm.PermissionInfo> queryPermissionsByGroup(java.lang.String, int) throws android.content.pm.PackageManager.NameNotFoundException;
method public abstract void registerDexModule(java.lang.String, android.content.pm.PackageManager.DexModuleRegisterCallback);
method public abstract void removeOnPermissionsChangeListener(android.content.pm.PackageManager.OnPermissionsChangedListener);
method public abstract deprecated void removePackageFromPreferred(java.lang.String);
method public abstract void removePermission(java.lang.String);
@@ -11559,6 +11560,11 @@ package android.content.pm {
field public static final int VERSION_CODE_HIGHEST = -1; // 0xffffffff
}
public static abstract class PackageManager.DexModuleRegisterCallback {
ctor public PackageManager.DexModuleRegisterCallback();
method public abstract void onDexModuleRegistered(java.lang.String, boolean, java.lang.String);
}
public static class PackageManager.NameNotFoundException extends android.util.AndroidException {
ctor public PackageManager.NameNotFoundException();
ctor public PackageManager.NameNotFoundException(java.lang.String);
@@ -44558,6 +44564,7 @@ package android.test.mock {
method public java.util.List<android.content.pm.ResolveInfo> queryIntentContentProviders(android.content.Intent, int);
method public java.util.List<android.content.pm.ResolveInfo> queryIntentServices(android.content.Intent, int);
method public java.util.List<android.content.pm.PermissionInfo> queryPermissionsByGroup(java.lang.String, int) throws android.content.pm.PackageManager.NameNotFoundException;
method public void registerDexModule(java.lang.String, android.content.pm.PackageManager.DexModuleRegisterCallback);
method public void removeOnPermissionsChangeListener(android.content.pm.PackageManager.OnPermissionsChangedListener);
method public void removePackageFromPreferred(java.lang.String);
method public void removePermission(java.lang.String);

View File

@@ -78,6 +78,10 @@ import android.os.UserManager;
import android.os.storage.StorageManager;
import android.os.storage.VolumeInfo;
import android.provider.Settings;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.system.StructStat;
import android.util.ArrayMap;
import android.util.IconDrawableFactory;
import android.util.LauncherIcons;
@@ -2662,4 +2666,78 @@ public class ApplicationPackageManager extends PackageManager {
throw e.rethrowAsRuntimeException();
}
}
private static class DexModuleRegisterResult {
final String dexModulePath;
final boolean success;
final String message;
private DexModuleRegisterResult(String dexModulePath, boolean success, String message) {
this.dexModulePath = dexModulePath;
this.success = success;
this.message = message;
}
}
private static class DexModuleRegisterCallbackDelegate
extends android.content.pm.IDexModuleRegisterCallback.Stub
implements Handler.Callback {
private static final int MSG_DEX_MODULE_REGISTERED = 1;
private final DexModuleRegisterCallback callback;
private final Handler mHandler;
DexModuleRegisterCallbackDelegate(@NonNull DexModuleRegisterCallback callback) {
this.callback = callback;
mHandler = new Handler(Looper.getMainLooper(), this);
}
@Override
public void onDexModuleRegistered(@NonNull String dexModulePath, boolean success,
@Nullable String message)throws RemoteException {
mHandler.obtainMessage(MSG_DEX_MODULE_REGISTERED,
new DexModuleRegisterResult(dexModulePath, success, message)).sendToTarget();
}
@Override
public boolean handleMessage(Message msg) {
if (msg.what != MSG_DEX_MODULE_REGISTERED) {
return false;
}
DexModuleRegisterResult result = (DexModuleRegisterResult)msg.obj;
callback.onDexModuleRegistered(result.dexModulePath, result.success, result.message);
return true;
}
}
@Override
public void registerDexModule(@NonNull String dexModule,
@Nullable DexModuleRegisterCallback callback) {
// Check if this is a shared module by looking if the others can read it.
boolean isSharedModule = false;
try {
StructStat stat = Os.stat(dexModule);
if ((OsConstants.S_IROTH & stat.st_mode) != 0) {
isSharedModule = true;
}
} catch (ErrnoException e) {
callback.onDexModuleRegistered(dexModule, false,
"Could not get stat the module file: " + e.getMessage());
return;
}
// Module path is ok.
// Create the callback delegate to be passed to package manager service.
DexModuleRegisterCallbackDelegate callbackDelegate = null;
if (callback != null) {
callbackDelegate = new DexModuleRegisterCallbackDelegate(callback);
}
// Invoke the package manager service.
try {
mPM.registerDexModule(mContext.getPackageName(), dexModule,
isSharedModule, callbackDelegate);
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
}
}

View File

@@ -0,0 +1,28 @@
/*
** Copyright 2017, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
** http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
package android.content.pm;
import android.os.Bundle;
/**
* Callback for registering a dex module with the Package Manager.
*
* @hide
*/
oneway interface IDexModuleRegisterCallback {
void onDexModuleRegistered(in String dexModulePath, in boolean success, in String message);
}

View File

@@ -25,6 +25,7 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.ChangedPackages;
import android.content.pm.InstantAppInfo;
import android.content.pm.FeatureInfo;
import android.content.pm.IDexModuleRegisterCallback;
import android.content.pm.IPackageInstallObserver2;
import android.content.pm.IPackageInstaller;
import android.content.pm.IPackageDeleteObserver;
@@ -473,6 +474,38 @@ interface IPackageManager {
*/
void notifyDexLoad(String loadingPackageName, in List<String> dexPaths, String loaderIsa);
/**
* Register an application dex module with the package manager.
* The package manager will keep track of the given module for future optimizations.
*
* Dex module optimizations will disable the classpath checking at runtime. The client bares
* the responsibility to ensure that the static assumptions on classes in the optimized code
* hold at runtime (e.g. there's no duplicate classes in the classpath).
*
* Note that the package manager already keeps track of dex modules loaded with
* {@link dalvik.system.DexClassLoader} and {@link dalvik.system.PathClassLoader}.
* This can be called for an eager registration.
*
* The call might take a while and the results will be posted on the main thread, using
* the given callback.
*
* If the module is intended to be shared with other apps, make sure that the file
* permissions allow for it.
* If at registration time the permissions allow for others to read it, the module would
* be marked as a shared module which might undergo a different optimization strategy.
* (usually shared modules will generated larger optimizations artifacts,
* taking more disk space).
*
* @param packageName the package name to which the dex module belongs
* @param dexModulePath the absolute path of the dex module.
* @param isSharedModule whether or not the module is intended to be used by other apps.
* @param callback if not null,
* {@link android.content.pm.IDexModuleRegisterCallback.IDexModuleRegisterCallback#onDexModuleRegistered}
* will be called once the registration finishes.
*/
void registerDexModule(in String packageName, in String dexModulePath,
in boolean isSharedModule, IDexModuleRegisterCallback callback);
/**
* Ask the package manager to perform a dex-opt for the given reason. The package
* manager will map the reason to a compiler filter according to the current system

View File

@@ -5725,4 +5725,48 @@ public abstract class PackageManager {
* @hide
*/
public abstract String getInstantAppAndroidId(String packageName, @NonNull UserHandle user);
/**
* Callback use to notify the callers of module registration that the operation
* has finished.
*
* @hide
*/
@SystemApi
public static abstract class DexModuleRegisterCallback {
public abstract void onDexModuleRegistered(String dexModulePath, boolean success,
String message);
}
/**
* Register an application dex module with the package manager.
* The package manager will keep track of the given module for future optimizations.
*
* Dex module optimizations will disable the classpath checking at runtime. The client bares
* the responsibility to ensure that the static assumptions on classes in the optimized code
* hold at runtime (e.g. there's no duplicate classes in the classpath).
*
* Note that the package manager already keeps track of dex modules loaded with
* {@link dalvik.system.DexClassLoader} and {@link dalvik.system.PathClassLoader}.
* This can be called for an eager registration.
*
* The call might take a while and the results will be posted on the main thread, using
* the given callback.
*
* If the module is intended to be shared with other apps, make sure that the file
* permissions allow for it.
* If at registration time the permissions allow for others to read it, the module would
* be marked as a shared module which might undergo a different optimization strategy.
* (usually shared modules will generated larger optimizations artifacts,
* taking more disk space).
*
* @param dexModulePath the absolute path of the dex module.
* @param callback if not null, {@link DexModuleRegisterCallback#onDexModuleRegistered} will
* be called once the registration finishes.
*
* @hide
*/
@SystemApi
public abstract void registerDexModule(String dexModulePath,
@Nullable DexModuleRegisterCallback callback);
}

View File

@@ -36,6 +36,7 @@ import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.FileUtils;
@@ -64,9 +65,14 @@ import android.util.Log;
import com.android.frameworks.coretests.R;
import com.android.internal.content.PackageHelper;
import dalvik.system.VMRuntime;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -78,9 +84,9 @@ public class PackageManagerTests extends AndroidTestCase {
public static final String TAG = "PackageManagerTests";
public final long MAX_WAIT_TIME = 25 * 1000;
public static final long MAX_WAIT_TIME = 25 * 1000;
public final long WAIT_TIME_INCR = 5 * 1000;
public static final long WAIT_TIME_INCR = 5 * 1000;
private static final String SECURE_CONTAINERS_PREFIX = "/mnt/asec";
@@ -3861,6 +3867,117 @@ public class PackageManagerTests extends AndroidTestCase {
PackageInfo.INSTALL_LOCATION_UNSPECIFIED);
}
private static class TestDexModuleRegisterCallback
extends PackageManager.DexModuleRegisterCallback {
private String mDexModulePath = null;
private boolean mSuccess = false;
private String mMessage = null;
CountDownLatch doneSignal = new CountDownLatch(1);
@Override
public void onDexModuleRegistered(String dexModulePath, boolean success, String message) {
mDexModulePath = dexModulePath;
mSuccess = success;
mMessage = message;
doneSignal.countDown();
}
boolean waitTillDone() {
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() - startTime < MAX_WAIT_TIME) {
try {
return doneSignal.await(MAX_WAIT_TIME, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Log.i(TAG, "Interrupted during sleep", e);
}
}
return false;
}
}
// Verify that the base code path cannot be registered.
public void testRegisterDexModuleBaseCode() throws Exception {
PackageManager pm = getPm();
ApplicationInfo info = getContext().getApplicationInfo();
TestDexModuleRegisterCallback callback = new TestDexModuleRegisterCallback();
pm.registerDexModule(info.getBaseCodePath(), callback);
assertTrue(callback.waitTillDone());
assertEquals(info.getBaseCodePath(), callback.mDexModulePath);
assertFalse("BaseCodePath should not be registered", callback.mSuccess);
}
// Verify thatmodules which are not own by the calling package are not registered.
public void testRegisterDexModuleNotOwningModule() throws Exception {
TestDexModuleRegisterCallback callback = new TestDexModuleRegisterCallback();
String moduleBelongingToOtherPackage = "/data/user/0/com.google.android.gms/module.apk";
getPm().registerDexModule(moduleBelongingToOtherPackage, callback);
assertTrue(callback.waitTillDone());
assertEquals(moduleBelongingToOtherPackage, callback.mDexModulePath);
assertTrue(callback.waitTillDone());
assertFalse("Only modules belonging to the calling package can be registered",
callback.mSuccess);
}
// Verify that modules owned by the package are successfully registered.
public void testRegisterDexModuleSuccessfully() throws Exception {
ApplicationInfo info = getContext().getApplicationInfo();
// Copy the main apk into the data folder and use it as a "module".
File dexModuleDir = new File(info.dataDir, "module-dir");
File dexModule = new File(dexModuleDir, "module.apk");
try {
assertNotNull(FileUtils.createDir(
dexModuleDir.getParentFile(), dexModuleDir.getName()));
Files.copy(Paths.get(info.getBaseCodePath()), dexModule.toPath(),
StandardCopyOption.REPLACE_EXISTING);
TestDexModuleRegisterCallback callback = new TestDexModuleRegisterCallback();
getPm().registerDexModule(dexModule.toString(), callback);
assertTrue(callback.waitTillDone());
assertEquals(dexModule.toString(), callback.mDexModulePath);
assertTrue(callback.waitTillDone());
assertTrue(callback.mMessage, callback.mSuccess);
// NOTE:
// This actually verifies internal behaviour which might change. It's not
// ideal but it's the best we can do since there's no other place we can currently
// write a better test.
for(String isa : getAppDexInstructionSets(info)) {
Files.exists(Paths.get(dexModuleDir.toString(), "oat", isa, "module.odex"));
Files.exists(Paths.get(dexModuleDir.toString(), "oat", isa, "module.vdex"));
}
} finally {
FileUtils.deleteContentsAndDir(dexModuleDir);
}
}
// If the module does not exist on disk we should get a failure.
public void testRegisterDexModuleNotExists() throws Exception {
ApplicationInfo info = getContext().getApplicationInfo();
String nonExistentApk = Paths.get(info.dataDir, "non-existent.apk").toString();
TestDexModuleRegisterCallback callback = new TestDexModuleRegisterCallback();
getPm().registerDexModule(nonExistentApk, callback);
assertTrue(callback.waitTillDone());
assertEquals(nonExistentApk, callback.mDexModulePath);
assertTrue(callback.waitTillDone());
assertFalse("DexModule registration should fail", callback.mSuccess);
}
// Copied from com.android.server.pm.InstructionSets because we don't have access to it here.
private static String[] getAppDexInstructionSets(ApplicationInfo info) {
if (info.primaryCpuAbi != null) {
if (info.secondaryCpuAbi != null) {
return new String[] {
VMRuntime.getInstructionSet(info.primaryCpuAbi),
VMRuntime.getInstructionSet(info.secondaryCpuAbi) };
} else {
return new String[] {
VMRuntime.getInstructionSet(info.primaryCpuAbi) };
}
}
return new String[] { VMRuntime.getInstructionSet(Build.SUPPORTED_ABIS[0]) };
}
/*---------- Recommended install location tests ----*/
/*
* TODO's

View File

@@ -34,7 +34,7 @@ import dalvik.system.VMRuntime;
*/
public class InstructionSets {
private static final String PREFERRED_INSTRUCTION_SET =
VMRuntime.getInstructionSet(Build.SUPPORTED_ABIS[0]);;
VMRuntime.getInstructionSet(Build.SUPPORTED_ABIS[0]);
public static String[] getAppDexInstructionSets(ApplicationInfo info) {
if (info.primaryCpuAbi != null) {
if (info.secondaryCpuAbi != null) {

View File

@@ -127,6 +127,7 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.AppsQueryHelper;
import android.content.pm.ChangedPackages;
import android.content.pm.ComponentInfo;
import android.content.pm.IDexModuleRegisterCallback;
import android.content.pm.InstantAppRequest;
import android.content.pm.AuxiliaryResolveInfo;
import android.content.pm.FallbackCategoryProvider;
@@ -8614,6 +8615,31 @@ public class PackageManagerService extends IPackageManager.Stub
mDexManager.notifyDexLoad(ai, dexPaths, loaderIsa, userId);
}
@Override
public void registerDexModule(String packageName, String dexModulePath, boolean isSharedModule,
IDexModuleRegisterCallback callback) {
int userId = UserHandle.getCallingUserId();
ApplicationInfo ai = getApplicationInfo(packageName, /*flags*/ 0, userId);
DexManager.RegisterDexModuleResult result;
if (ai == null) {
Slog.w(TAG, "Registering a dex module for a package that does not exist for the" +
" calling user. package=" + packageName + ", user=" + userId);
result = new DexManager.RegisterDexModuleResult(false, "Package not installed");
} else {
result = mDexManager.registerDexModule(ai, dexModulePath, isSharedModule, userId);
}
if (callback != null) {
mHandler.post(() -> {
try {
callback.onDexModuleRegistered(dexModulePath, result.success, result.message);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to callback after module registration " + dexModulePath, e);
}
});
}
}
@Override
public boolean performDexOpt(String packageName,
boolean checkProfiles, int compileReason, boolean force) {

View File

@@ -30,6 +30,7 @@ import com.android.internal.annotations.GuardedBy;
import com.android.server.pm.Installer;
import com.android.server.pm.Installer.InstallerException;
import com.android.server.pm.PackageDexOptimizer;
import com.android.server.pm.PackageManagerService;
import com.android.server.pm.PackageManagerServiceUtils;
import com.android.server.pm.PackageManagerServiceCompilerMapping;
@@ -41,6 +42,7 @@ import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static com.android.server.pm.InstructionSets.getAppDexInstructionSets;
import static com.android.server.pm.dex.PackageDexUsage.PackageUseInfo;
import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo;
@@ -431,6 +433,52 @@ public class DexManager {
}
}
public RegisterDexModuleResult registerDexModule(ApplicationInfo info, String dexPath,
boolean isUsedByOtherApps, int userId) {
// Find the owning package record.
DexSearchResult searchResult = getDexPackage(info, dexPath, userId);
if (searchResult.mOutcome == DEX_SEARCH_NOT_FOUND) {
return new RegisterDexModuleResult(false, "Package not found");
}
if (!info.packageName.equals(searchResult.mOwningPackageName)) {
return new RegisterDexModuleResult(false, "Dex path does not belong to package");
}
if (searchResult.mOutcome == DEX_SEARCH_FOUND_PRIMARY ||
searchResult.mOutcome == DEX_SEARCH_FOUND_SPLIT) {
return new RegisterDexModuleResult(false, "Main apks cannot be registered");
}
// We found the package. Now record the usage for all declared ISAs.
boolean update = false;
Set<String> isas = new HashSet<>();
for (String isa : getAppDexInstructionSets(info)) {
isas.add(isa);
boolean newUpdate = mPackageDexUsage.record(searchResult.mOwningPackageName,
dexPath, userId, isa, isUsedByOtherApps, /*primaryOrSplit*/ false);
update |= newUpdate;
}
if (update) {
mPackageDexUsage.maybeWriteAsync();
}
// Try to optimize the package according to the install reason.
String compilerFilter = PackageManagerServiceCompilerMapping.getCompilerFilterForReason(
PackageManagerService.REASON_INSTALL);
int result = mPackageDexOptimizer.dexOptSecondaryDexPath(info, dexPath, isas,
compilerFilter, isUsedByOtherApps);
// If we fail to optimize the package log an error but don't propagate the error
// back to the app. The app cannot do much about it and the background job
// will rety again when it executes.
// TODO(calin): there might be some value to return the error here but it may
// cause red herrings since that doesn't mean the app cannot use the module.
if (result != PackageDexOptimizer.DEX_OPT_FAILED) {
Slog.e(TAG, "Failed to optimize dex module " + dexPath);
}
return new RegisterDexModuleResult(true, "Dex module registered successfully");
}
/**
* Return all packages that contain records of secondary dex files.
*/
@@ -510,6 +558,20 @@ public class DexManager {
return existingValue == null ? newValue : existingValue;
}
public static class RegisterDexModuleResult {
public RegisterDexModuleResult() {
this(false, null);
}
public RegisterDexModuleResult(boolean success, String message) {
this.success = success;
this.message = message;
}
public final boolean success;
public final String message;
}
/**
* Convenience class to store the different locations where a package might
* own code.
@@ -589,6 +651,4 @@ public class DexManager {
return mOwningPackageName + "-" + mOutcome;
}
}
}

View File

@@ -17,6 +17,7 @@
package android.test.mock;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.PackageInstallObserver;
import android.content.ComponentName;
import android.content.Intent;
@@ -1159,4 +1160,13 @@ public class MockPackageManager extends PackageManager {
public String getInstantAppAndroidId(String packageName, UserHandle user) {
throw new UnsupportedOperationException();
}
/**
* @hide
*/
@Override
public void registerDexModule(String dexModulePath,
@Nullable DexModuleRegisterCallback callback) {
throw new UnsupportedOperationException();
}
}

View File

@@ -949,4 +949,10 @@ public class BridgePackageManager extends PackageManager {
public String getInstantAppAndroidId(String packageName, UserHandle user) {
return null;
}
@Override
public void registerDexModule(String dexModulePath,
@Nullable DexModuleRegisterCallback callback) {
callback.onDexModuleRegistered(dexModulePath, false, null);
}
}