Merge changes Id7d1ff26,Id53a2e65 into rvc-dev am: bde104f8be am: 1eeef296d2 am: 0f31ef312c

Change-Id: I15614e3fa703f02e18c0bf504c1bc032dc9dd0d9
This commit is contained in:
Winson Chiu
2020-04-01 06:16:48 +00:00
committed by Automerger Merge Worker
35 changed files with 1247 additions and 192 deletions

View File

@@ -22,6 +22,7 @@ import android.content.pm.PackageParserCacheHelper.WriteHelper
import android.content.pm.parsing.ParsingPackageImpl
import android.content.pm.parsing.ParsingPackageRead
import android.content.pm.parsing.ParsingPackageUtils
import android.content.pm.parsing.result.ParseInput
import android.content.pm.parsing.result.ParseTypeImpl
import android.content.res.TypedArray
import android.perftests.utils.BenchmarkState
@@ -173,7 +174,10 @@ class PackageParsingPerfTest {
class ParallelParser2(cacher: PackageCacher2? = null)
: ParallelParser<ParsingPackageRead>(cacher) {
val input = ThreadLocal.withInitial { ParseTypeImpl() }
val input = ThreadLocal.withInitial {
// For testing, just disable enforcement to avoid hooking up to compat framework
ParseTypeImpl(ParseInput.Callback { _, _, _ -> false })
}
val parser = ParsingPackageUtils(false, null, null,
object : ParsingPackageUtils.Callback {
override fun hasFeature(feature: String) = true

View File

@@ -65,7 +65,9 @@ import android.content.pm.parsing.component.ParsedProviderUtils;
import android.content.pm.parsing.component.ParsedService;
import android.content.pm.parsing.component.ParsedServiceUtils;
import android.content.pm.parsing.result.ParseInput;
import android.content.pm.parsing.result.ParseInput.DeferredError;
import android.content.pm.parsing.result.ParseResult;
import android.content.pm.parsing.result.ParseTypeImpl;
import android.content.pm.permission.SplitPermissionInfoParcelable;
import android.content.pm.split.DefaultSplitAssetLoader;
import android.content.pm.split.SplitAssetDependencyLoader;
@@ -126,6 +128,51 @@ public class ParsingPackageUtils {
public static final String TAG = ParsingUtils.TAG;
/**
* For cases outside of PackageManagerService when an APK needs to be parsed as a one-off
* request, without caching the input object and without querying the internal system state
* for feature support.
*/
@NonNull
public static ParseResult<ParsingPackage> parseDefaultOneTime(File file, int flags,
@NonNull ParseInput.Callback inputCallback, @NonNull Callback callback) {
if ((flags & (PackageManager.MATCH_DIRECT_BOOT_UNAWARE
| PackageManager.MATCH_DIRECT_BOOT_AWARE)) == 0) {
// Caller expressed no opinion about what encryption
// aware/unaware components they want to see, so match both
flags |= PackageManager.MATCH_DIRECT_BOOT_AWARE
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
}
ParseInput input = new ParseTypeImpl(inputCallback).reset();
ParseResult<ParsingPackage> result;
ParsingPackageUtils parser = new ParsingPackageUtils(false, null, null, callback);
try {
result = parser.parsePackage(input, file, flags);
if (result.isError()) {
return result;
}
} catch (PackageParser.PackageParserException e) {
return input.error(PackageManager.INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
"Error parsing package", e);
}
try {
ParsingPackage pkg = result.getResult();
if ((flags & PackageManager.GET_SIGNATURES) != 0
|| (flags & PackageManager.GET_SIGNING_CERTIFICATES) != 0) {
ParsingPackageUtils.collectCertificates(pkg, false /* skipVerify */);
}
return input.success(pkg);
} catch (PackageParser.PackageParserException e) {
return input.error(PackageManager.INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
"Error collecting package certificates", e);
}
}
private boolean mOnlyCoreApps;
private String[] mSeparateProcesses;
private DisplayMetrics mDisplayMetrics;
@@ -456,10 +503,11 @@ public class ParsingPackageUtils {
}
if (!foundApp) {
return input.error(
PackageManager.INSTALL_PARSE_FAILED_MANIFEST_EMPTY,
"<manifest> does not contain an <application>"
);
ParseResult<?> deferResult = input.deferError(
"<manifest> does not contain an <application>", DeferredError.MISSING_APP_TAG);
if (deferResult.isError()) {
return input.error(deferResult);
}
}
return input.success(pkg);
@@ -663,10 +711,12 @@ public class ParsingPackageUtils {
}
if (!foundApp && ArrayUtils.size(pkg.getInstrumentations()) == 0) {
return input.error(
PackageManager.INSTALL_PARSE_FAILED_MANIFEST_EMPTY,
"<manifest> does not contain an <application> or <instrumentation>"
);
ParseResult<?> deferResult = input.deferError(
"<manifest> does not contain an <application> or <instrumentation>",
DeferredError.MISSING_APP_TAG);
if (deferResult.isError()) {
return input.error(deferResult);
}
}
if (!ParsedAttribution.isCombinationValid(pkg.getAttributions())) {
@@ -758,11 +808,9 @@ public class ParsingPackageUtils {
return input.success(pkg);
}
ParseResult nameResult = validateName(input, str, true, true);
if (nameResult.isError()) {
if ("android".equals(pkg.getPackageName())) {
nameResult.ignoreError();
} else {
if (!"android".equals(pkg.getPackageName())) {
ParseResult<?> nameResult = validateName(input, str, true, true);
if (nameResult.isError()) {
return input.error(PackageManager.INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID,
"<manifest> specifies bad sharedUserId name \"" + str + "\": "
+ nameResult.getErrorMessage());
@@ -1166,6 +1214,20 @@ public class ParsingPackageUtils {
targetCode = minCode;
}
ParseResult<Integer> targetSdkVersionResult = computeTargetSdkVersion(
targetVers, targetCode, PackageParser.SDK_CODENAMES, input);
if (targetSdkVersionResult.isError()) {
return input.error(targetSdkVersionResult);
}
int targetSdkVersion = targetSdkVersionResult.getResult();
ParseResult<?> deferResult =
input.enableDeferredError(pkg.getPackageName(), targetSdkVersion);
if (deferResult.isError()) {
return input.error(deferResult);
}
ParseResult<Integer> minSdkVersionResult = computeMinSdkVersion(minVers, minCode,
PackageParser.SDK_VERSION, PackageParser.SDK_CODENAMES, input);
if (minSdkVersionResult.isError()) {
@@ -1174,14 +1236,6 @@ public class ParsingPackageUtils {
int minSdkVersion = minSdkVersionResult.getResult();
ParseResult<Integer> targetSdkVersionResult = computeTargetSdkVersion(
targetVers, targetCode, PackageParser.SDK_CODENAMES, input);
if (targetSdkVersionResult.isError()) {
return input.error(targetSdkVersionResult);
}
int targetSdkVersion = minSdkVersionResult.getResult();
pkg.setMinSdkVersion(minSdkVersion)
.setTargetSdkVersion(targetSdkVersion);
@@ -1763,9 +1817,15 @@ public class ParsingPackageUtils {
// Add a hidden app detail activity to normal apps which forwards user to App Details
// page.
ParseResult<ParsedActivity> a = generateAppDetailsHiddenActivity(input, pkg);
// Backwards-compat, assume success
if (a.isError()) {
// Error should be impossible here, as the only failure case as of SDK R is a
// string validation error on a constant ":app_details" string passed in by the
// parsing code itself. For this reason, this is just a hard failure instead of
// deferred.
return input.error(a);
}
pkg.addActivity(a.getResult());
a.ignoreError();
}
if (hasActivityOrder) {
@@ -2122,18 +2182,14 @@ public class ParsingPackageUtils {
private static ParseResult<ParsedActivity> generateAppDetailsHiddenActivity(ParseInput input,
ParsingPackage pkg) {
String packageName = pkg.getPackageName();
ParseResult<String> taskAffinityResult = ComponentParseUtils.buildTaskAffinityName(
ParseResult<String> result = ComponentParseUtils.buildTaskAffinityName(
packageName, packageName, ":app_details", input);
String taskAffinity;
if (taskAffinityResult.isSuccess()) {
taskAffinity = taskAffinityResult.getResult();
} else {
// Backwards-compat, do not fail
taskAffinity = null;
taskAffinityResult.ignoreError();
if (result.isError()) {
return input.error(result);
}
String taskAffinity = result.getResult();
// Build custom App Details activity info instead of parsing it from xml
return input.success(ParsedActivity.makeAppDetailsActivity(packageName,
pkg.getProcessName(), pkg.getUiOptions(), taskAffinity,
@@ -2688,7 +2744,8 @@ public class ParsingPackageUtils {
public interface Callback {
boolean hasFeature(String feature);
ParsingPackage startParsingPackage(String packageName, String baseCodePath, String codePath,
ParsingPackage startParsingPackage(@NonNull String packageName,
@NonNull String baseCodePath, @NonNull String codePath,
@NonNull TypedArray manifestArray, boolean isCoreApp);
}
}

View File

@@ -179,13 +179,12 @@ public class ParsedActivityUtils {
ParseResult<String> affinityNameResult = ComponentParseUtils.buildTaskAffinityName(
packageName, pkg.getTaskAffinity(), taskAffinity, input);
if (affinityNameResult.isSuccess()) {
activity.taskAffinity = affinityNameResult.getResult();
} else {
// Backwards-compat, ignore error
affinityNameResult.ignoreError();
if (affinityNameResult.isError()) {
return input.error(affinityNameResult);
}
activity.taskAffinity = affinityNameResult.getResult();
boolean visibleToEphemeral = sa.getBoolean(R.styleable.AndroidManifestActivity_visibleToInstantApps, false);
if (visibleToEphemeral) {
activity.flags |= ActivityInfo.FLAG_VISIBLE_TO_INSTANT_APP;

View File

@@ -29,7 +29,6 @@ import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.os.PatternMatcher;
import android.text.TextUtils;
import android.util.Slog;
import android.util.TypedValue;
@@ -97,8 +96,13 @@ public class ParsedIntentInfoUtils {
case "action": {
String value = parser.getAttributeValue(PackageParser.ANDROID_RESOURCES,
"name");
if (TextUtils.isEmpty(value)) {
if (value == null) {
result = input.error("No value supplied for <android:name>");
} else if (value.isEmpty()) {
intentInfo.addAction(value);
// Prior to R, this was not a failure
result = input.deferError("No value supplied for <android:name>",
ParseInput.DeferredError.EMPTY_INTENT_ACTION_CATEGORY);
} else {
intentInfo.addAction(value);
result = input.success(null);
@@ -108,8 +112,13 @@ public class ParsedIntentInfoUtils {
case "category": {
String value = parser.getAttributeValue(PackageParser.ANDROID_RESOURCES,
"name");
if (TextUtils.isEmpty(value)) {
if (value == null) {
result = input.error("No value supplied for <android:name>");
} else if (value.isEmpty()) {
intentInfo.addCategory(value);
// Prior to R, this was not a failure
result = input.deferError("No value supplied for <android:name>",
ParseInput.DeferredError.EMPTY_INTENT_ACTION_CATEGORY);
} else {
intentInfo.addCategory(value);
result = input.success(null);

View File

@@ -20,6 +20,9 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.IntentFilter;
import android.content.pm.parsing.ParsingPackage;
import android.content.pm.parsing.ParsingPackageUtils;
import android.content.pm.parsing.result.ParseInput;
import android.content.pm.parsing.result.ParseResult;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
@@ -28,9 +31,6 @@ import android.os.Build;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import android.content.pm.parsing.ParsingPackageUtils;
import android.content.pm.parsing.result.ParseInput;
import android.content.pm.parsing.result.ParseResult;
import org.xmlpull.v1.XmlPullParserException;
@@ -83,12 +83,11 @@ class ParsedMainComponentUtils {
ParseResult<String> processNameResult = ComponentParseUtils.buildProcessName(
pkg.getPackageName(), pkg.getProcessName(), processName, flags,
separateProcesses, input);
if (processNameResult.isSuccess()) {
component.setProcessName(processNameResult.getResult());
} else {
// Backwards-compat, ignore error
processNameResult.ignoreError();
if (processNameResult.isError()) {
return input.error(processNameResult);
}
component.setProcessName(processNameResult.getResult());
}
if (splitNameAttr != null) {

View File

@@ -16,10 +16,12 @@
package android.content.pm.parsing.result;
import android.annotation.Hide;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledAfter;
import android.content.pm.PackageManager;
import android.os.Build;
/**
* Used as a method parameter which is then transformed into a {@link ParseResult}. This is
@@ -30,8 +32,52 @@ import android.content.pm.PackageManager;
*/
public interface ParseInput {
/**
* Errors encountered during parsing may rely on the targetSDK version of the application to
* determine whether or not to fail. These are passed into {@link #deferError(String, long)}
* when encountered, and the implementation will handle how to defer the errors until the
* targetSdkVersion is known and sent to {@link #enableDeferredError(String, int)}.
*
* All of these must be marked {@link ChangeId}, as that is the mechanism used to check if the
* error must be propagated. This framework also allows developers to pre-disable specific
* checks if they wish to target a newer SDK version in a development environment without
* having to migrate their entire app to validate on a newer platform.
*/
final class DeferredError {
/**
* Missing an "application" or "instrumentation" tag.
*/
@ChangeId
@EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
public static final long MISSING_APP_TAG = 150776642;
/**
* An intent filter's actor or category is an empty string. A bug in the platform before R
* allowed this to pass through without an error. This does not include cases when the
* attribute is null/missing, as that has always been a failure.
*/
@ChangeId
@EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
public static final long EMPTY_INTENT_ACTION_CATEGORY = 151163173;
}
<ResultType> ParseResult<ResultType> success(ResultType result);
/**
* Used for errors gated by {@link DeferredError}. Will return an error result if the
* targetSdkVersion is already known and this must be returned as a real error. The result
* contains null and should not be unwrapped.
*
* @see #error(String)
*/
ParseResult<?> deferError(@NonNull String parseError, long deferredError);
/**
* Called after targetSdkVersion is known. Returns an error result if a previously deferred
* error was registered. The result contains null and should not be unwrapped.
*/
ParseResult<?> enableDeferredError(String packageName, int targetSdkVersion);
/** @see #error(int, String, Exception) */
<ResultType> ParseResult<ResultType> error(int parseError);
@@ -52,9 +98,6 @@ public interface ParseInput {
* The calling site of that method is then expected to check the result for error, and
* continue to bubble up if it is an error.
*
* Or, if the code explicitly handles an error,
* {@link ParseResult#ignoreError()} should be called.
*
* If the result {@link ParseResult#isSuccess()}, then it can be used as-is, as
* overlapping/consecutive successes are allowed.
*/
@@ -66,5 +109,17 @@ public interface ParseInput {
* but cast the type of the {@link ParseResult} for type safety, since the parameter
* and the receiver should be the same object.
*/
<ResultType> ParseResult<ResultType> error(ParseResult result);
<ResultType> ParseResult<ResultType> error(ParseResult<?> result);
/**
* Implemented instead of a direct reference to
* {@link com.android.internal.compat.IPlatformCompat}, allowing caching and testing logic to
* be separated out.
*/
interface Callback {
/**
* @return true if the changeId should be enabled
*/
boolean isChangeEnabled(long changeId, @NonNull String packageName, int targetSdkVersion);
}
}

View File

@@ -17,31 +17,18 @@
package android.content.pm.parsing.result;
import android.annotation.Nullable;
import android.content.pm.PackageParser;
/**
* The output side of {@link ParseInput}, which must result from a method call on
* {@link ParseInput}.
*
* When using this class, keep in mind that all {@link ParseInput}s and {@link ParseResult}s
* are the exact same object, scoped to a per {@link PackageParser} instance, per thread basis,
* thrown around and casted everywhere for type safety.
* are the exact same object, scoped per thread, thrown around and casted for type safety.
*
* @hide
*/
public interface ParseResult<ResultType> {
/**
* Un-marks this result as an error, also allowing it to be re-used as {@link ParseInput}.
*
* This should only be used in cases where it's absolutely certain that error handling is
* irrelevant. Such as for backwards compatibility where it previously didn't fail and that
* behavior has to be maintained.
*
* Mostly an alias for readability.
*/
void ignoreError();
/**
* Returns true if the result is not an error and thus contains a valid object.
*

View File

@@ -20,51 +20,130 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.pm.PackageManager;
import android.content.pm.parsing.ParsingUtils;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Slog;
import java.util.Arrays;
import com.android.internal.util.CollectionUtils;
/** @hide */
public class ParseTypeImpl implements ParseInput, ParseResult<Object> {
private static final String TAG = ParsingUtils.TAG;
private static final boolean DEBUG_FILL_STACK_TRACE = false;
public static final boolean DEBUG_FILL_STACK_TRACE = false;
private static final boolean DEBUG_LOG_ON_ERROR = false;
public static final boolean DEBUG_LOG_ON_ERROR = false;
private Object result;
public static final boolean DEBUG_THROW_ALL_ERRORS = false;
private int errorCode = PackageManager.INSTALL_SUCCEEDED;
@NonNull
private Callback mCallback;
private Object mResult;
private int mErrorCode = PackageManager.INSTALL_SUCCEEDED;
@Nullable
private String errorMessage;
private String mErrorMessage;
@Nullable
private Exception exception;
private Exception mException;
/**
* Errors encountered before targetSdkVersion is known.
* The size upper bound is the number of longs in {@link DeferredError}
*/
@Nullable
private ArrayMap<Long, String> mDeferredErrors = null;
private String mPackageName;
private Integer mTargetSdkVersion;
/**
* @param callback if nullable, fallback to manual targetSdk > Q check
*/
public ParseTypeImpl(@NonNull Callback callback) {
mCallback = callback;
}
public ParseInput reset() {
this.result = null;
this.errorCode = PackageManager.INSTALL_SUCCEEDED;
this.errorMessage = null;
this.exception = null;
mResult = null;
mErrorCode = PackageManager.INSTALL_SUCCEEDED;
mErrorMessage = null;
mException = null;
if (mDeferredErrors != null) {
// If the memory was already allocated, don't bother freeing and re-allocating,
// as this could occur hundreds of times depending on what the caller is doing and
// how many APKs they're going through.
mDeferredErrors.erase();
}
return this;
}
@Override
public void ignoreError() {
reset();
public <ResultType> ParseResult<ResultType> success(ResultType result) {
if (mErrorCode != PackageManager.INSTALL_SUCCEEDED) {
Slog.wtf(ParsingUtils.TAG, "Cannot set to success after set to error, was "
+ mErrorMessage, mException);
}
mResult = result;
//noinspection unchecked
return (ParseResult<ResultType>) this;
}
@Override
public <ResultType> ParseResult<ResultType> success(ResultType result) {
if (errorCode != PackageManager.INSTALL_SUCCEEDED || errorMessage != null) {
throw new IllegalStateException("Cannot set to success after set to error, was "
+ errorMessage, exception);
public ParseResult<?> deferError(@NonNull String parseError, long deferredError) {
if (DEBUG_THROW_ALL_ERRORS) {
return error(parseError);
}
this.result = result;
//noinspection unchecked
return (ParseResult<ResultType>) this;
if (mTargetSdkVersion != null) {
if (mDeferredErrors != null && mDeferredErrors.containsKey(deferredError)) {
// If the map already contains the key, that means it's already been checked and
// found to be disabled. Otherwise it would've failed when mTargetSdkVersion was
// set to non-null.
return success(null);
}
if (mCallback.isChangeEnabled(deferredError, mPackageName, mTargetSdkVersion)) {
return error(parseError);
} else {
if (mDeferredErrors == null) {
mDeferredErrors = new ArrayMap<>();
}
mDeferredErrors.put(deferredError, null);
return success(null);
}
}
if (mDeferredErrors == null) {
mDeferredErrors = new ArrayMap<>();
}
// Only save the first occurrence of any particular error
mDeferredErrors.putIfAbsent(deferredError, parseError);
return success(null);
}
@Override
public ParseResult<?> enableDeferredError(String packageName, int targetSdkVersion) {
mPackageName = packageName;
mTargetSdkVersion = targetSdkVersion;
int size = CollectionUtils.size(mDeferredErrors);
for (int index = size - 1; index >= 0; index--) {
long changeId = mDeferredErrors.keyAt(index);
String errorMessage = mDeferredErrors.valueAt(index);
if (mCallback.isChangeEnabled(changeId, mPackageName, mTargetSdkVersion)) {
return error(errorMessage);
} else {
// No point holding onto the string, but need to maintain the key to signal
// that the error was checked with isChangeEnabled and found to be disabled.
mDeferredErrors.setValueAt(index, null);
}
}
return success(null);
}
@Override
@@ -84,25 +163,26 @@ public class ParseTypeImpl implements ParseInput, ParseResult<Object> {
}
@Override
public <ResultType> ParseResult<ResultType> error(ParseResult intentResult) {
return error(intentResult.getErrorCode(), intentResult.getErrorMessage());
public <ResultType> ParseResult<ResultType> error(ParseResult<?> intentResult) {
return error(intentResult.getErrorCode(), intentResult.getErrorMessage(),
intentResult.getException());
}
@Override
public <ResultType> ParseResult<ResultType> error(int errorCode, @Nullable String errorMessage,
Exception exception) {
this.errorCode = errorCode;
this.errorMessage = errorMessage;
this.exception = exception;
mErrorCode = errorCode;
mErrorMessage = errorMessage;
mException = exception;
if (DEBUG_FILL_STACK_TRACE) {
if (exception == null) {
this.exception = new Exception();
mException = new Exception();
}
}
if (DEBUG_LOG_ON_ERROR) {
Exception exceptionToLog = this.exception != null ? this.exception : new Exception();
Exception exceptionToLog = mException != null ? mException : new Exception();
Log.w(TAG, "ParseInput set to error " + errorCode + ", " + errorMessage,
exceptionToLog);
}
@@ -113,12 +193,12 @@ public class ParseTypeImpl implements ParseInput, ParseResult<Object> {
@Override
public Object getResult() {
return this.result;
return mResult;
}
@Override
public boolean isSuccess() {
return errorCode == PackageManager.INSTALL_SUCCEEDED;
return mErrorCode == PackageManager.INSTALL_SUCCEEDED;
}
@Override
@@ -128,18 +208,18 @@ public class ParseTypeImpl implements ParseInput, ParseResult<Object> {
@Override
public int getErrorCode() {
return errorCode;
return mErrorCode;
}
@Nullable
@Override
public String getErrorMessage() {
return errorMessage;
return mErrorMessage;
}
@Nullable
@Override
public Exception getException() {
return exception;
return mException;
}
}

View File

@@ -0,0 +1,281 @@
/*
* Copyright (C) 2020 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.parsing.result
import android.content.pm.PackageManager
import android.os.Build
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Assume.assumeFalse
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.mockito.ArgumentMatchers.anyLong
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.anyString
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import java.io.IOException
class ParseInputAndResultTest {
companion object {
private const val TEST_PACKAGE = "com.android.test"
private const val ENABLED_ERROR = 11L
private const val DISABLED_ERROR = 22L
@JvmStatic
@BeforeClass
fun assumeNotDebug() {
// None of these tests consider cases where debugging logic is enabled
assumeFalse(ParseTypeImpl.DEBUG_FILL_STACK_TRACE)
assumeFalse(ParseTypeImpl.DEBUG_LOG_ON_ERROR)
assumeFalse(ParseTypeImpl.DEBUG_THROW_ALL_ERRORS)
}
}
private lateinit var mockCallback: ParseInput.Callback
private lateinit var input: ParseInput
@Before
fun createInput() {
// Use an open class instead off a lambda so it can be spied
open class TestCallback : ParseInput.Callback {
override fun isChangeEnabled(changeId: Long, pkgName: String, targetSdk: Int): Boolean {
return when (changeId) {
ENABLED_ERROR -> targetSdk > Build.VERSION_CODES.Q
DISABLED_ERROR -> false
else -> throw IllegalStateException("changeId $changeId is not mocked for test")
}
}
}
mockCallback = spy(TestCallback())
input = ParseTypeImpl(mockCallback)
}
@Test
fun errorCode() {
val errorCode = PackageManager.INSTALL_FAILED_UPDATE_INCOMPATIBLE
val result = input.error<Any?>(errorCode)
assertError(result)
assertThat(result.errorCode).isEqualTo(errorCode)
assertThat(result.errorMessage).isNull()
assertThat(result.exception).isNull()
}
@Test
fun errorMessage() {
val errorMessage = "Test error"
val result = input.error<Any?>(errorMessage)
assertError(result)
assertThat(result.errorCode).isNotEqualTo(PackageManager.INSTALL_SUCCEEDED)
assertThat(result.errorMessage).isEqualTo(errorMessage)
assertThat(result.exception).isNull()
}
@Test
fun errorCodeAndMessage() {
val errorCode = PackageManager.INSTALL_FAILED_UPDATE_INCOMPATIBLE
val errorMessage = "Test error"
val result = input.error<Any?>(errorCode, errorMessage)
assertError(result)
assertThat(result.errorCode).isEqualTo(errorCode)
assertThat(result.errorMessage).isEqualTo(errorMessage)
assertThat(result.exception).isNull()
}
@Test
fun errorCodeAndMessageAndException() {
val errorCode = PackageManager.INSTALL_FAILED_UPDATE_INCOMPATIBLE
val errorMessage = "Test error"
val exception = IOException()
val result = input.error<Any?>(errorCode, errorMessage, exception)
assertError(result)
assertThat(result.errorCode).isEqualTo(errorCode)
assertThat(result.errorMessage).isEqualTo(errorMessage)
assertThat(result.exception).isSameAs(exception)
}
@Test
fun errorCarryResult() {
val errorCode = PackageManager.INSTALL_FAILED_UPDATE_INCOMPATIBLE
val errorMessage = "Test error"
val exception = IOException()
val result = input.error<Any?>(errorCode, errorMessage, exception)
assertError(result)
assertThat(result.errorCode).isEqualTo(errorCode)
assertThat(result.errorMessage).isEqualTo(errorMessage)
assertThat(result.exception).isSameAs(exception)
val carriedResult = input.error<Int>(result)
assertError(carriedResult)
assertThat(carriedResult.errorCode).isEqualTo(errorCode)
assertThat(carriedResult.errorMessage).isEqualTo(errorMessage)
assertThat(carriedResult.exception).isSameAs(exception)
}
@Test
fun success() {
val value = "Test success"
assertSuccess(value, input.success(value))
}
@Test
fun deferErrorEnableFirstSdkQ() {
assertSuccess(input.enableDeferredError(TEST_PACKAGE, Build.VERSION_CODES.Q))
assertSuccess(input.deferError("Test error", ENABLED_ERROR))
}
@Test
fun deferErrorEnableLastSdkQ() {
assertSuccess(input.deferError("Test error", ENABLED_ERROR))
assertSuccess(input.enableDeferredError(TEST_PACKAGE, Build.VERSION_CODES.Q))
}
@Test
fun deferErrorEnableFirstSdkR() {
val error = "Test error"
assertSuccess(input.enableDeferredError(TEST_PACKAGE, Build.VERSION_CODES.R))
val deferResult = input.deferError(error, ENABLED_ERROR)
assertError(deferResult)
assertThat(deferResult.errorCode).isNotEqualTo(PackageManager.INSTALL_SUCCEEDED)
assertThat(deferResult.errorMessage).isEqualTo(error)
assertThat(deferResult.exception).isNull()
}
@Test
fun deferErrorEnableLastSdkR() {
val error = "Test error"
assertSuccess(input.deferError(error, ENABLED_ERROR))
val result = input.enableDeferredError(TEST_PACKAGE, Build.VERSION_CODES.R)
assertError(result)
assertThat(result.errorCode).isNotEqualTo(PackageManager.INSTALL_SUCCEEDED)
assertThat(result.errorMessage).isEqualTo(error)
assertThat(result.exception).isNull()
}
@Test
fun enableDeferredErrorAndSuccessSdkQ() {
val value = "Test success"
assertSuccess(input.enableDeferredError(TEST_PACKAGE, Build.VERSION_CODES.Q))
assertSuccess(value, input.success(value))
}
@Test
fun enableDeferredErrorAndSuccessSdkR() {
val value = "Test success"
assertSuccess(input.enableDeferredError(TEST_PACKAGE, Build.VERSION_CODES.R))
assertSuccess(value, input.success(value))
}
@Test
fun multipleDeferErrorKeepsFirst() {
val errorOne = "Test error one"
val errorTwo = "Test error two"
assertSuccess(input.deferError(errorOne, ENABLED_ERROR))
assertSuccess(input.deferError(errorTwo, ENABLED_ERROR))
val result = input.enableDeferredError(TEST_PACKAGE, Build.VERSION_CODES.R)
assertError(result)
assertThat(result.errorCode).isNotEqualTo(PackageManager.INSTALL_SUCCEEDED)
assertThat(result.errorMessage).isEqualTo(errorOne)
assertThat(result.exception).isNull()
}
@Test
fun multipleDisabledErrorsQueriesOnceEnableFirst() {
val errorOne = "Test error one"
val errorTwo = "Test error two"
assertSuccess(input.enableDeferredError(TEST_PACKAGE, Build.VERSION_CODES.R))
assertSuccess(input.deferError(errorOne, DISABLED_ERROR))
verify(mockCallback, times(1)).isChangeEnabled(anyLong(), anyString(), anyInt())
assertSuccess(input.deferError(errorTwo, DISABLED_ERROR))
verifyNoMoreInteractions(mockCallback)
}
@Test
fun multipleDisabledErrorsQueriesOnceEnableSecond() {
val errorOne = "Test error one"
val errorTwo = "Test error two"
assertSuccess(input.deferError(errorOne, DISABLED_ERROR))
verify(mockCallback, never()).isChangeEnabled(anyLong(), anyString(), anyInt())
assertSuccess(input.enableDeferredError(TEST_PACKAGE, Build.VERSION_CODES.R))
verify(mockCallback, times(1)).isChangeEnabled(anyLong(), anyString(), anyInt())
assertSuccess(input.deferError(errorTwo, DISABLED_ERROR))
verifyNoMoreInteractions(mockCallback)
}
@After
fun verifyReset() {
var result = (input as ParseTypeImpl).reset() as ParseResult<*>
result.assertReset()
// The deferred error is not directly accessible, so attempt to re-enable the deferred
// error and assert it was also reset.
result = input.enableDeferredError(TEST_PACKAGE, Build.VERSION_CODES.R)
result.assertReset()
}
private fun assertSuccess(result: ParseResult<*>) = assertSuccess(null, result)
private fun assertSuccess(expected: Any? = null, result: ParseResult<*>) {
assertThat(result.isError).isFalse()
assertThat(result.isSuccess).isTrue()
assertThat(result.result).isSameAs(expected)
assertThat(result.errorCode).isEqualTo(PackageManager.INSTALL_SUCCEEDED)
assertThat(result.errorMessage).isNull()
assertThat(result.exception).isNull()
}
private fun assertError(result: ParseResult<*>) {
assertThat(result.isError).isTrue()
assertThat(result.isSuccess).isFalse()
assertThat(result.result).isNull()
}
private fun ParseResult<*>.assertReset() {
assertThat(this.isSuccess).isTrue()
assertThat(this.isError).isFalse()
assertThat(this.errorCode).isEqualTo(PackageManager.INSTALL_SUCCEEDED)
assertThat(this.errorMessage).isNull()
assertThat(this.exception).isNull()
}
}

View File

@@ -91,6 +91,7 @@ import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.function.Supplier;
/** Implementation of {@link AppIntegrityManagerService}. */
public class AppIntegrityManagerServiceImpl extends IAppIntegrityManager.Stub {
@@ -125,6 +126,7 @@ public class AppIntegrityManagerServiceImpl extends IAppIntegrityManager.Stub {
private final Context mContext;
private final Handler mHandler;
private final PackageManagerInternal mPackageManagerInternal;
private final Supplier<PackageParser2> mParserSupplier;
private final RuleEvaluationEngine mEvaluationEngine;
private final IntegrityFileManager mIntegrityFileManager;
@@ -136,6 +138,7 @@ public class AppIntegrityManagerServiceImpl extends IAppIntegrityManager.Stub {
return new AppIntegrityManagerServiceImpl(
context,
LocalServices.getService(PackageManagerInternal.class),
PackageParser2::forParsingFileWithDefaults,
RuleEvaluationEngine.getRuleEvaluationEngine(),
IntegrityFileManager.getInstance(),
handlerThread.getThreadHandler());
@@ -145,11 +148,13 @@ public class AppIntegrityManagerServiceImpl extends IAppIntegrityManager.Stub {
AppIntegrityManagerServiceImpl(
Context context,
PackageManagerInternal packageManagerInternal,
Supplier<PackageParser2> parserSupplier,
RuleEvaluationEngine evaluationEngine,
IntegrityFileManager integrityFileManager,
Handler handler) {
mContext = context;
mPackageManagerInternal = packageManagerInternal;
mParserSupplier = parserSupplier;
mEvaluationEngine = evaluationEngine;
mIntegrityFileManager = integrityFileManager;
mHandler = handler;
@@ -562,8 +567,7 @@ public class AppIntegrityManagerServiceImpl extends IAppIntegrityManager.Stub {
throw new IllegalArgumentException("Installation path is null, package not found");
}
PackageParser2 parser = new PackageParser2(null, false, null, null, null);
try {
try (PackageParser2 parser = mParserSupplier.get()) {
ParsedPackage pkg = parser.parsePackage(installationPath, 0, false);
int flags = PackageManager.GET_SIGNING_CERTIFICATES | PackageManager.GET_META_DATA;
pkg.setSigningDetails(ParsingPackageUtils.collectCertificates(pkg, false));

View File

@@ -1035,13 +1035,7 @@ public class PackageManagerService extends IPackageManager.Stub
private final AppsFilter mAppsFilter;
class PackageParserCallback extends PackageParser2.Callback {
@Override public final boolean hasFeature(String feature) {
return PackageManagerService.this.hasSystemFeature(feature, 0);
}
}
final PackageParser2.Callback mPackageParserCallback = new PackageParserCallback();
final PackageParser2.Callback mPackageParserCallback;
// Currently known shared libraries.
final ArrayMap<String, LongSparseArray<SharedLibraryInfo>> mSharedLibraries = new ArrayMap<>();
@@ -2720,6 +2714,18 @@ public class PackageManagerService extends IPackageManager.Stub
mPermissionManagerService = (IPermissionManager) ServiceManager.getService("permissionmgr");
mIncrementalManager =
(IncrementalManager) mContext.getSystemService(Context.INCREMENTAL_SERVICE);
PlatformCompat platformCompat = mInjector.getCompatibility();
mPackageParserCallback = new PackageParser2.Callback() {
@Override
public boolean isChangeEnabled(long changeId, @NonNull ApplicationInfo appInfo) {
return platformCompat.isChangeEnabled(changeId, appInfo);
}
@Override
public boolean hasFeature(String feature) {
return PackageManagerService.this.hasSystemFeature(feature, 0);
}
};
// CHECKSTYLE:ON IndentationCheck
t.traceEnd();
@@ -3075,6 +3081,8 @@ public class PackageManagerService extends IPackageManager.Stub
}
packageParser.close();
List<Runnable> unfinishedTasks = executorService.shutdownNow();
if (!unfinishedTasks.isEmpty()) {
throw new IllegalStateException("Not all tasks finished before calling close: "
@@ -8901,12 +8909,11 @@ public class PackageManagerService extends IPackageManager.Stub
private AndroidPackage scanPackageLI(File scanFile, int parseFlags, int scanFlags,
long currentTime, UserHandle user) throws PackageManagerException {
if (DEBUG_INSTALL) Slog.d(TAG, "Parsing: " + scanFile);
PackageParser2 pp = new PackageParser2(mSeparateProcesses, mOnlyCore, mMetrics, null,
mPackageParserCallback);
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "parsePackage");
final ParsedPackage parsedPackage;
try {
try (PackageParser2 pp = new PackageParser2(mSeparateProcesses, mOnlyCore, mMetrics, null,
mPackageParserCallback)) {
parsedPackage = pp.parsePackage(scanFile, parseFlags, false);
} catch (PackageParserException e) {
throw PackageManagerException.from(e);
@@ -16676,12 +16683,10 @@ public class PackageManagerService extends IPackageManager.Stub
| PackageParser.PARSE_ENFORCE_CODE
| (onExternal ? PackageParser.PARSE_EXTERNAL_STORAGE : 0);
PackageParser2 pp = new PackageParser2(mSeparateProcesses, false, mMetrics, null,
mPackageParserCallback);
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "parsePackage");
ParsedPackage parsedPackage;
try {
try (PackageParser2 pp = new PackageParser2(mSeparateProcesses, false, mMetrics, null,
mPackageParserCallback)) {
parsedPackage = pp.parsePackage(tmpPackageFile, parseFlags, false);
AndroidPackageUtils.validatePackageDexMetadata(parsedPackage);
} catch (PackageParserException e) {
@@ -16746,8 +16751,6 @@ public class PackageManagerService extends IPackageManager.Stub
"Instant app package must be signed with APK Signature Scheme v2 or greater");
}
// Get rid of all references to package scan path via parser.
pp = null;
boolean systemApp = false;
boolean replace = false;
synchronized (mLock) {

View File

@@ -17,7 +17,10 @@
package com.android.server.pm.parsing;
import android.annotation.AnyThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageParser;
import android.content.pm.PackageParser.PackageParserException;
import android.content.pm.parsing.ParsingPackage;
@@ -27,10 +30,13 @@ import android.content.pm.parsing.result.ParseResult;
import android.content.pm.parsing.result.ParseTypeImpl;
import android.content.res.TypedArray;
import android.os.Build;
import android.os.ServiceManager;
import android.os.SystemClock;
import android.util.DisplayMetrics;
import android.util.Slog;
import com.android.server.compat.PlatformCompat;
import com.android.server.pm.PackageManagerService;
import com.android.server.pm.parsing.pkg.PackageImpl;
import com.android.server.pm.parsing.pkg.ParsedPackage;
@@ -39,19 +45,53 @@ import java.io.File;
/**
* The v2 of {@link PackageParser} for use when parsing is initiated in the server and must
* contain state contained by the server.
*
* The {@link AutoCloseable} helps signal that this class contains resources that must be freed.
* Although it is sufficient to release references to an instance of this class and let it get
* collected automatically.
*/
public class PackageParser2 {
public class PackageParser2 implements AutoCloseable {
/**
* For parsing inside the system server but outside of {@link PackageManagerService}.
* Generally used for parsing information in an APK that hasn't been installed yet.
*
* This must be called inside the system process as it relies on {@link ServiceManager}.
*/
public static PackageParser2 forParsingFileWithDefaults() {
PlatformCompat platformCompat =
(PlatformCompat) ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE);
return new PackageParser2(null /* separateProcesses */, false /* onlyCoreApps */,
null /* displayMetrics */, null /* cacheDir */, new Callback() {
@Override
public boolean isChangeEnabled(long changeId, @NonNull ApplicationInfo appInfo) {
return platformCompat.isChangeEnabled(changeId, appInfo);
}
@Override
public boolean hasFeature(String feature) {
// Assume the device doesn't support anything. This will affect permission parsing
// and will force <uses-permission/> declarations to include all requiredNotFeature
// permissions and exclude all requiredFeature permissions. This mirrors the old
// behavior.
return false;
}
});
}
static final String TAG = "PackageParser2";
private static final boolean LOG_PARSE_TIMINGS = Build.IS_DEBUGGABLE;
private static final int LOG_PARSE_TIMINGS_THRESHOLD_MS = 100;
private ThreadLocal<ParseTypeImpl> mSharedResult = ThreadLocal.withInitial(ParseTypeImpl::new);
private ThreadLocal<ApplicationInfo> mSharedAppInfo =
ThreadLocal.withInitial(() -> {
ApplicationInfo appInfo = new ApplicationInfo();
appInfo.uid = -1; // Not a valid UID since the app will not be installed yet
return appInfo;
});
private final String[] mSeparateProcesses;
private final boolean mOnlyCoreApps;
private final DisplayMetrics mDisplayMetrics;
private ThreadLocal<ParseTypeImpl> mSharedResult;
@Nullable
protected PackageCacher mCacher;
@@ -64,27 +104,26 @@ public class PackageParser2 {
* creating a minimalist boot environment.
*/
public PackageParser2(String[] separateProcesses, boolean onlyCoreApps,
DisplayMetrics displayMetrics, @Nullable File cacheDir, Callback callback) {
mSeparateProcesses = separateProcesses;
mOnlyCoreApps = onlyCoreApps;
DisplayMetrics displayMetrics, @Nullable File cacheDir, @NonNull Callback callback) {
if (displayMetrics == null) {
mDisplayMetrics = new DisplayMetrics();
mDisplayMetrics.setToDefaults();
} else {
mDisplayMetrics = displayMetrics;
displayMetrics = new DisplayMetrics();
displayMetrics.setToDefaults();
}
mCacher = cacheDir == null ? null : new PackageCacher(cacheDir);
// TODO(b/135203078): Remove nullability of callback
callback = callback != null ? callback : new Callback() {
@Override
public boolean hasFeature(String feature) {
return false;
}
parsingUtils = new ParsingPackageUtils(onlyCoreApps, separateProcesses, displayMetrics,
callback);
ParseInput.Callback enforcementCallback = (changeId, packageName, targetSdkVersion) -> {
ApplicationInfo appInfo = mSharedAppInfo.get();
//noinspection ConstantConditions
appInfo.packageName = packageName;
appInfo.targetSdkVersion = targetSdkVersion;
return callback.isChangeEnabled(changeId, appInfo);
};
parsingUtils = new ParsingPackageUtils(onlyCoreApps, separateProcesses, displayMetrics, callback);
mSharedResult = ThreadLocal.withInitial(() -> new ParseTypeImpl(enforcementCallback));
}
/**
@@ -126,13 +165,38 @@ public class PackageParser2 {
return parsed;
}
/**
* Removes the cached value for the thread the parser was created on. It is assumed that
* any threads created for parallel parsing will be created and released, so they don't
* need an explicit close call.
*
* Realistically an instance should never be retained, so when the enclosing class is released,
* the values will also be released, making this method unnecessary.
*/
@Override
public void close() {
mSharedResult.remove();
mSharedAppInfo.remove();
}
public static abstract class Callback implements ParsingPackageUtils.Callback {
@Override
public final ParsingPackage startParsingPackage(String packageName, String baseCodePath,
String codePath, TypedArray manifestArray, boolean isCoreApp) {
public final ParsingPackage startParsingPackage(@NonNull String packageName,
@NonNull String baseCodePath, @NonNull String codePath,
@NonNull TypedArray manifestArray, boolean isCoreApp) {
return PackageImpl.forParsing(packageName, baseCodePath, codePath, manifestArray,
isCoreApp);
}
/**
* An indirection from {@link ParseInput.Callback#isChangeEnabled(long, String, int)},
* allowing the {@link ApplicationInfo} objects to be cached in {@link #mSharedAppInfo}
* and cleaned up with the parser instance, not the callback instance.
*
* @param appInfo will only have 3 fields filled in, {@link ApplicationInfo#packageName},
* {@link ApplicationInfo#targetSdkVersion}, and {@link ApplicationInfo#uid}
*/
public abstract boolean isChangeEnabled(long changeId, @NonNull ApplicationInfo appInfo);
}
}

View File

@@ -67,8 +67,11 @@ import android.provider.Settings;
import androidx.test.InstrumentationRegistry;
import com.android.internal.R;
import com.android.server.compat.PlatformCompat;
import com.android.server.integrity.engine.RuleEvaluationEngine;
import com.android.server.integrity.model.IntegrityCheckResult;
import com.android.server.pm.parsing.PackageParser2;
import com.android.server.pm.parsing.TestPackageParser2;
import com.android.server.testutils.TestUtils;
import org.junit.After;
@@ -88,6 +91,7 @@ import java.nio.file.Files;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
/** Unit test for {@link com.android.server.integrity.AppIntegrityManagerServiceImpl} */
@RunWith(JUnit4.class)
@@ -131,12 +135,15 @@ public class AppIntegrityManagerServiceImplTest {
@org.junit.Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
@Mock PackageManagerInternal mPackageManagerInternal;
@Mock PlatformCompat mPlatformCompat;
@Mock Context mMockContext;
@Mock Resources mMockResources;
@Mock RuleEvaluationEngine mRuleEvaluationEngine;
@Mock IntegrityFileManager mIntegrityFileManager;
@Mock Handler mHandler;
private Supplier<PackageParser2> mParserSupplier = TestPackageParser2::new;
private final Context mRealContext = InstrumentationRegistry.getTargetContext();
private PackageManager mSpyPackageManager;
@@ -168,6 +175,7 @@ public class AppIntegrityManagerServiceImplTest {
new AppIntegrityManagerServiceImpl(
mMockContext,
mPackageManagerInternal,
mParserSupplier,
mRuleEvaluationEngine,
mIntegrityFileManager,
mHandler);

View File

@@ -46,6 +46,7 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.frameworks.servicestests.R;
import com.android.server.pm.parsing.PackageParser2;
import com.android.server.pm.parsing.TestPackageParser2;
import org.junit.Before;
import org.junit.Test;
@@ -77,7 +78,7 @@ public class ApexManagerTest {
ApexManager.ApexManagerImpl managerImpl = spy(new ApexManager.ApexManagerImpl());
doReturn(mApexService).when(managerImpl).waitForApexService();
mApexManager = managerImpl;
mPackageParser2 = new PackageParser2(null, false, null, null, null);
mPackageParser2 = new TestPackageParser2();
}
@Test

View File

@@ -49,7 +49,6 @@ import android.os.Bundle;
import android.os.Parcel;
import android.platform.test.annotations.Presubmit;
import android.util.ArraySet;
import android.util.DisplayMetrics;
import androidx.annotation.Nullable;
import androidx.test.InstrumentationRegistry;
@@ -61,6 +60,7 @@ import com.android.internal.util.ArrayUtils;
import com.android.server.pm.parsing.PackageCacher;
import com.android.server.pm.parsing.PackageInfoUtils;
import com.android.server.pm.parsing.PackageParser2;
import com.android.server.pm.parsing.TestPackageParser2;
import com.android.server.pm.parsing.pkg.AndroidPackage;
import com.android.server.pm.parsing.pkg.PackageImpl;
import com.android.server.pm.parsing.pkg.ParsedPackage;
@@ -108,7 +108,7 @@ public class PackageParserTest {
@Test
public void testParse_noCache() throws Exception {
CachePackageNameParser pp = new CachePackageNameParser(null, false, null, null, null);
CachePackageNameParser pp = new CachePackageNameParser(null);
ParsedPackage pkg = pp.parsePackage(FRAMEWORK, 0 /* parseFlags */,
false /* useCaches */);
assertNotNull(pkg);
@@ -125,7 +125,7 @@ public class PackageParserTest {
@Test
public void testParse_withCache() throws Exception {
CachePackageNameParser pp = new CachePackageNameParser(null, false, null, null, null);
CachePackageNameParser pp = new CachePackageNameParser(null);
pp.setCacheDir(mTmpDir);
// The first parse will write this package to the cache.
@@ -144,7 +144,7 @@ public class PackageParserTest {
// We haven't set a cache directory here : the parse should still succeed,
// just not using the cached results.
pp = new CachePackageNameParser(null, false, null, null, null);
pp = new CachePackageNameParser(null);
pkg = pp.parsePackage(FRAMEWORK, 0 /* parseFlags */, true /* useCaches */);
assertEquals("android", pkg.getPackageName());
@@ -154,17 +154,29 @@ public class PackageParserTest {
@Test
public void test_serializePackage() throws Exception {
PackageParser2 pp = new PackageParser2(null, false, null, mTmpDir, null);
ParsedPackage pkg = pp.parsePackage(FRAMEWORK, 0 /* parseFlags */,
true /* useCaches */);
try (PackageParser2 pp = new PackageParser2(null, false, null, mTmpDir,
new PackageParser2.Callback() {
@Override
public boolean isChangeEnabled(long changeId, @NonNull ApplicationInfo appInfo) {
return true;
}
Parcel p = Parcel.obtain();
pkg.writeToParcel(p, 0 /* flags */);
@Override
public boolean hasFeature(String feature) {
return false;
}
})) {
ParsedPackage pkg = pp.parsePackage(FRAMEWORK, 0 /* parseFlags */,
true /* useCaches */);
p.setDataPosition(0);
ParsedPackage deserialized = new PackageImpl(p);
Parcel p = Parcel.obtain();
pkg.writeToParcel(p, 0 /* flags */);
assertPackagesEqual(pkg, deserialized);
p.setDataPosition(0);
ParsedPackage deserialized = new PackageImpl(p);
assertPackagesEqual(pkg, deserialized);
}
}
@Test
@@ -218,10 +230,6 @@ public class PackageParserTest {
assertSame(deserialized.getSharedUserId(), deserialized2.getSharedUserId());
}
private static PackageParser2 makeParser() {
return new PackageParser2(null, false, null, null, null);
}
private File extractFile(String filename) throws Exception {
final Context context = InstrumentationRegistry.getTargetContext();
final File tmpFile = File.createTempFile(filename, ".apk");
@@ -238,7 +246,7 @@ public class PackageParserTest {
public void testParseIsolatedSplitsDefault() throws Exception {
final File testFile = extractFile(TEST_APP1_APK);
try {
final ParsedPackage pkg = makeParser().parsePackage(testFile, 0, false);
final ParsedPackage pkg = new TestPackageParser2().parsePackage(testFile, 0, false);
assertFalse("isolatedSplits", pkg.isIsolatedSplitLoading());
} finally {
testFile.delete();
@@ -252,7 +260,7 @@ public class PackageParserTest {
public void testParseIsolatedSplitsConstant() throws Exception {
final File testFile = extractFile(TEST_APP2_APK);
try {
final ParsedPackage pkg = makeParser().parsePackage(testFile, 0, false);
final ParsedPackage pkg = new TestPackageParser2().parsePackage(testFile, 0, false);
assertTrue("isolatedSplits", pkg.isIsolatedSplitLoading());
} finally {
testFile.delete();
@@ -266,7 +274,7 @@ public class PackageParserTest {
public void testParseIsolatedSplitsResource() throws Exception {
final File testFile = extractFile(TEST_APP3_APK);
try {
final ParsedPackage pkg = makeParser().parsePackage(testFile, 0, false);
final ParsedPackage pkg = new TestPackageParser2().parsePackage(testFile, 0, false);
assertTrue("isolatedSplits", pkg.isIsolatedSplitLoading());
} finally {
testFile.delete();
@@ -279,10 +287,18 @@ public class PackageParserTest {
*/
public static class CachePackageNameParser extends PackageParser2 {
CachePackageNameParser(String[] separateProcesses, boolean onlyCoreApps,
DisplayMetrics displayMetrics, @Nullable File cacheDir,
PackageParser2.Callback callback) {
super(separateProcesses, onlyCoreApps, displayMetrics, cacheDir, callback);
CachePackageNameParser(@Nullable File cacheDir) {
super(null, false, null, null, new Callback() {
@Override
public boolean isChangeEnabled(long changeId, @NonNull ApplicationInfo appInfo) {
return true;
}
@Override
public boolean hasFeature(String feature) {
return false;
}
});
if (cacheDir != null) {
setCacheDir(cacheDir);
}

View File

@@ -16,13 +16,13 @@
package com.android.server.pm;
import android.content.pm.PackageParser;
import android.platform.test.annotations.Presubmit;
import android.util.Log;
import androidx.test.runner.AndroidJUnit4;
import com.android.server.pm.parsing.PackageParser2;
import com.android.server.pm.parsing.TestPackageParser2;
import com.android.server.pm.parsing.pkg.ParsedPackage;
import junit.framework.Assert;
@@ -48,7 +48,7 @@ public class ParallelPackageParserTest {
@Before
public void setUp() {
mParser = new TestParallelPackageParser(new PackageParser2(null, false, null, null, null),
mParser = new TestParallelPackageParser(new TestPackageParser2(),
ParallelPackageParser.makeExecutorService());
}

View File

@@ -35,7 +35,7 @@ import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.frameworks.servicestests.R;
import com.android.server.pm.parsing.PackageParser2;
import com.android.server.pm.parsing.TestPackageParser2;
import com.android.server.pm.parsing.pkg.AndroidPackageUtils;
import com.android.server.pm.parsing.pkg.ParsedPackage;
@@ -93,15 +93,11 @@ public class DexMetadataHelperTest {
return outFile;
}
private PackageParser2 makeParser() {
return new PackageParser2(null, false, null, null, null);
}
@Test
public void testParsePackageWithDmFileValid() throws IOException, PackageParserException {
copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base);
createDexMetadataFile("install_split_base.apk");
ParsedPackage pkg = makeParser().parsePackage(mTmpDir, 0 /* flags */, false);
ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, 0 /* flags */, false);
Map<String, String> packageDexMetadata = AndroidPackageUtils.getPackageDexMetadata(pkg);
assertEquals(1, packageDexMetadata.size());
@@ -117,7 +113,7 @@ public class DexMetadataHelperTest {
copyApkToToTmpDir("install_split_feature_a.apk", R.raw.install_split_feature_a);
createDexMetadataFile("install_split_base.apk");
createDexMetadataFile("install_split_feature_a.apk");
ParsedPackage pkg = makeParser().parsePackage(mTmpDir, 0 /* flags */, false);
ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, 0 /* flags */, false);
Map<String, String> packageDexMetadata = AndroidPackageUtils.getPackageDexMetadata(pkg);
assertEquals(2, packageDexMetadata.size());
@@ -136,7 +132,7 @@ public class DexMetadataHelperTest {
copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base);
copyApkToToTmpDir("install_split_feature_a.apk", R.raw.install_split_feature_a);
createDexMetadataFile("install_split_feature_a.apk");
ParsedPackage pkg = makeParser().parsePackage(mTmpDir, 0 /* flags */, false);
ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, 0 /* flags */, false);
Map<String, String> packageDexMetadata = AndroidPackageUtils.getPackageDexMetadata(pkg);
assertEquals(1, packageDexMetadata.size());
@@ -152,7 +148,8 @@ public class DexMetadataHelperTest {
File invalidDmFile = new File(mTmpDir, "install_split_base.dm");
Files.createFile(invalidDmFile.toPath());
try {
ParsedPackage pkg = makeParser().parsePackage(mTmpDir, 0 /* flags */, false);
ParsedPackage pkg = new TestPackageParser2()
.parsePackage(mTmpDir, 0 /* flags */, false);
AndroidPackageUtils.validatePackageDexMetadata(pkg);
} catch (PackageParserException e) {
assertEquals(e.error, PackageManager.INSTALL_FAILED_BAD_DEX_METADATA);
@@ -169,7 +166,8 @@ public class DexMetadataHelperTest {
Files.createFile(invalidDmFile.toPath());
try {
ParsedPackage pkg = makeParser().parsePackage(mTmpDir, 0 /* flags */, false);
ParsedPackage pkg = new TestPackageParser2()
.parsePackage(mTmpDir, 0 /* flags */, false);
AndroidPackageUtils.validatePackageDexMetadata(pkg);
} catch (PackageParserException e) {
assertEquals(e.error, PackageManager.INSTALL_FAILED_BAD_DEX_METADATA);

View File

@@ -47,8 +47,7 @@ open class AndroidPackageParsingTestBase {
companion object {
// TODO(chiuwinson): Enable in separate change to fail all presubmit builds and fix errors
private const val VERIFY_ALL_APKS = false
private const val VERIFY_ALL_APKS = true
/** For auditing memory usage differences */
private const val DUMP_HPROF_TO_EXTERNAL = false
@@ -57,13 +56,10 @@ open class AndroidPackageParsingTestBase {
protected val packageParser = PackageParser().apply {
setOnlyCoreApps(false)
setDisplayMetrics(context.resources.displayMetrics)
setCallback { true }
setCallback { false /* hasFeature */ }
}
protected val packageParser2 = PackageParser2(null, false, context.resources.displayMetrics,
null, object : PackageParser2.Callback() {
override fun hasFeature(feature: String?) = true
})
protected val packageParser2 = TestPackageParser2()
/**
* It would be difficult to mock all possibilities, so just use the APKs on device.

View File

@@ -102,10 +102,6 @@ public class PackageParserLegacyCoreTest {
}
}
private PackageParser2 makeParser() {
return new PackageParser2(null, false, null, null, null);
}
@Test
public void testComputeMinSdkVersion_preReleasePlatform() {
// Do allow older release minSdkVersion on pre-release platform.
@@ -357,8 +353,8 @@ public class PackageParserLegacyCoreTest {
File outFile = null;
try {
outFile = copyRawResourceToFile(apkFileName, apkResourceId);
return converter.apply(
makeParser().parsePackage(outFile, 0 /* flags */, false));
return converter.apply(new TestPackageParser2()
.parsePackage(outFile, 0 /* flags */, false));
} finally {
if (outFile != null) {
outFile.delete();

View File

@@ -0,0 +1,149 @@
/*
* Copyright (C) 2020 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 com.android.server.pm.parsing
import android.annotation.RawRes
import android.content.Context
import android.content.pm.parsing.ParsingPackage
import android.content.pm.parsing.ParsingPackageImpl
import android.content.pm.parsing.ParsingPackageUtils
import android.content.pm.parsing.result.ParseInput
import android.content.pm.parsing.result.ParseInput.DeferredError
import android.content.pm.parsing.result.ParseResult
import android.content.res.TypedArray
import android.os.Build
import androidx.test.InstrumentationRegistry
import com.android.frameworks.servicestests.R
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
/**
* There are 2 known errors when parsing a manifest that were promoted to true failures in R:
* 1. Missing an <application> or <instrumentation> tag
* 2. An empty string action/category in an intent-filter
*
* This verifies these failures when the APK targets R.
*/
class PackageParsingDeferErrorTest {
companion object {
private const val TEST_ACTIVITY =
"com.android.servicestests.pm.parsing.test.TestActivity"
private const val TEST_ACTION =
"com.android.servicestests.pm.parsing.test.TEST_ACTION"
private const val TEST_CATEGORY =
"com.android.servicestests.pm.parsing.test.TEST_CATEGORY"
private const val TEST_PERMISSION =
"com.android.servicestests.pm.parsing.missingapp.TEST_PERMISSION"
}
private val context: Context = InstrumentationRegistry.getContext()
private val inputCallback = ParseInput.Callback { changeId, _, targetSdk ->
when (changeId) {
DeferredError.MISSING_APP_TAG -> targetSdk > Build.VERSION_CODES.Q
DeferredError.EMPTY_INTENT_ACTION_CATEGORY -> targetSdk > Build.VERSION_CODES.Q
else -> throw IllegalStateException("changeId $changeId is not mocked for test")
}
}
private val parsingCallback = object : ParsingPackageUtils.Callback {
override fun hasFeature(feature: String?) = true
override fun startParsingPackage(
packageName: String,
baseCodePath: String,
codePath: String,
manifestArray: TypedArray,
isCoreApp: Boolean
): ParsingPackage {
return ParsingPackageImpl(packageName, baseCodePath, codePath, manifestArray)
}
}
@get:Rule
val tempFolder = TemporaryFolder(context.filesDir)
@Test
fun emptyIntentFilterActionSdkQ() {
val result = parseFile(R.raw.PackageParsingTestAppEmptyActionSdkQ)
assertWithMessage(result.errorMessage).that(result.isError).isFalse()
val activities = result.result.activities
// 2 because of AppDetailsActivity
assertThat(activities).hasSize(2)
val first = activities.first()
assertThat(first.name).isEqualTo(TEST_ACTIVITY)
val intents = first.intents
assertThat(intents).hasSize(1)
assertThat(intents.first().hasCategory(TEST_CATEGORY)).isTrue()
assertThat(intents.first().hasAction(TEST_ACTION)).isTrue()
}
@Test
fun emptyIntentFilterActionSdkR() {
val result = parseFile(R.raw.PackageParsingTestAppEmptyActionSdkR)
assertThat(result.isError).isTrue()
}
@Test
fun emptyIntentFilterCategorySdkQ() {
val result = parseFile(R.raw.PackageParsingTestAppEmptyCategorySdkQ)
assertWithMessage(result.errorMessage).that(result.isError).isFalse()
val activities = result.result.activities
// 2 because of AppDetailsActivity
assertThat(activities).hasSize(2)
val first = activities.first()
assertThat(first.name).isEqualTo(TEST_ACTIVITY)
val intents = first.intents
assertThat(intents).hasSize(1)
assertThat(intents.first().hasAction(TEST_ACTION)).isTrue()
}
@Test
fun emptyIntentFilterCategorySdkR() {
val result = parseFile(R.raw.PackageParsingTestAppEmptyCategorySdkR)
assertThat(result.isError).isTrue()
}
@Test
fun missingAppTagSdkQ() {
val result = parseFile(R.raw.PackageParsingTestAppMissingAppSdkQ)
assertWithMessage(result.errorMessage).that(result.isError).isFalse()
val permissions = result.result.permissions
assertThat(permissions).hasSize(1)
assertThat(permissions.first().name).isEqualTo(TEST_PERMISSION)
}
@Test
fun missingAppTagSdkR() {
val result = parseFile(R.raw.PackageParsingTestAppMissingAppSdkR)
assertThat(result.isError).isTrue()
}
private fun parseFile(@RawRes id: Int): ParseResult<ParsingPackage> {
val file = tempFolder.newFile()
context.resources.openRawResource(id).use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
return ParsingPackageUtils.parseDefaultOneTime(file, 0, inputCallback, parsingCallback)
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) 2020 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 com.android.server.pm.parsing
import android.content.pm.ApplicationInfo
class TestPackageParser2 : PackageParser2(null /* separateProcesses */, false /* onlyCoreApps */,
null /* displayMetrics */, null /* cacheDir */, object : PackageParser2.Callback() {
override fun isChangeEnabled(changeId: Long, appInfo: ApplicationInfo): Boolean {
return true
}
override fun hasFeature(feature: String): Boolean {
// Assume the device doesn't support anything. This will affect permission parsing
// and will force <uses-permission/> declarations to include all requiredNotFeature
// permissions and exclude all requiredFeature permissions. This mirrors the old
// behavior.
return false
}
})

View File

@@ -0,0 +1,70 @@
// Copyright (C) 2019 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.
// NOTE: NONE_OF_THESE_TARGETS_ACTUALLY_WORK. AAPT2 doesn't seem to be able to skip
// errors within tag attributes. This is here as an reference of how to build the test apps, but
// they will have to built manually and checked into the tree as prebuilts. A modified version of
// AAPT2 is necessary to build the broken APKs.
// android_test_helper_app {
// name: "PackageParsingTestAppEmptyActionSdkQ",
// manifest: "AndroidManifestEmptyAction.xml",
// srcs: ["**/*.kt"],
// min_sdk_version: "28",
// target_sdk_version: "29",
// aaptflags: ["--warn-manifest-validation"],
// }
// android_test_helper_app {
// name: "PackageParsingTestAppEmptyActionSdkR",
// manifest: "AndroidManifestEmptyAction.xml",
// srcs: ["**/*.kt"],
// min_sdk_version: "28",
// target_sdk_version: "30",
// aaptflags: ["--warn-manifest-validation"],
// }
// android_test_helper_app {
// name: "PackageParsingTestAppEmptyCategorySdkQ",
// manifest: "AndroidManifestEmptyCategory.xml",
// srcs: ["**/*.kt"],
// min_sdk_version: "28",
// target_sdk_version: "29",
// aaptflags: ["--warn-manifest-validation"],
// }
// android_test_helper_app {
// name: "PackageParsingTestAppEmptyCategorySdkR",
// manifest: "AndroidManifestEmptyCategory.xml",
// srcs: ["**/*.kt"],
// min_sdk_version: "28",
// target_sdk_version: "30",
// aaptflags: ["--warn-manifest-validation"],
// }
// android_test_helper_app {
// name: "PackageParsingTestAppMissingAppSdkQ",
// manifest: "AndroidManifestMissingApp.xml",
// min_sdk_version: "28",
// target_sdk_version: "29",
// aaptflags: ["--warn-manifest-validation"],
// }
// android_test_helper_app {
// name: "PackageParsingTestAppMissingAppSdkR",
// manifest: "AndroidManifestMissingApp.xml",
// min_sdk_version: "28",
// target_sdk_version: "30",
// aaptflags: ["--warn-manifest-validation"],
// }

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2020 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.servicestests.pm.parsing.emptyaction">
<application>
<activity android:name="com.android.servicestests.pm.parsing.test.TestActivity">
<intent-filter>
<action android:name="" />
<!-- Non-empty action use to verify filter, since 0 action filters are stripped -->
<action android:name="com.android.servicestests.pm.parsing.test.TEST_ACTION" />
<category android:name="com.android.servicestests.pm.parsing.test.TEST_CATEGORY"/>
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2020 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.servicestests.pm.parsing.emptycategory">
<application>
<activity android:name="com.android.servicestests.pm.parsing.test.TestActivity">
<intent-filter>
<action android:name="com.android.servicestests.pm.parsing.test.TEST_ACTION"/>
<category android:name="" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2020 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.servicestests.pm.parsing.missingapp">
<!-- Only to assert that the manifest parsed correctly -->
<permission android:name="com.android.servicestests.pm.parsing.missingapp.TEST_PERMISSION"
android:protectionLevel="normal" />
</manifest>

View File

@@ -0,0 +1,19 @@
/*
* Copyright (C) 2020 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 com.android.servicestests.pm.parsing.test
class TestActivity

View File

@@ -17,7 +17,10 @@
android_test {
name: "PlatformCompatGating",
// Only compile source java files in this apk.
srcs: ["src/**/*.java"],
srcs: [
"src/**/*.java",
"src/**/*.kt",
],
test_suites: ["device-tests"],
static_libs: [
"junit",
@@ -25,7 +28,8 @@ android_test {
"androidx.test.core",
"androidx.test.ext.junit",
"mockito-target-minus-junit4",
"testng",
"truth-prebuilt",
"platform-compat-test-rules"
"platform-compat-test-rules",
],
}

View File

@@ -2,6 +2,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.tests.gating">
<queries>
<package android:name="com.android.tests.gating.app_not_installed" />
</queries>
<application android:label="GatingTest">
<uses-library android:name="android.test.runner" />
</application>

View File

@@ -0,0 +1,133 @@
/*
* Copyright (C) 2020 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 com.android.tests.gating
import android.Manifest
import android.app.UiAutomation
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.content.pm.parsing.result.ParseInput
import android.os.Build
import android.os.ParcelFileDescriptor
import android.os.ServiceManager
import androidx.test.platform.app.InstrumentationRegistry
import com.android.internal.compat.IPlatformCompat
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.testng.Assert.assertThrows
import java.io.FileReader
/**
* Verifies the shell commands "am compat enable/disable/reset" against a real server change ID
* for a not installed package.
*
* This class intentionally does not use any PlatformCompat testing infrastructure since that could
* interfere with what it's testing.
*/
@RunWith(Parameterized::class)
class PlatformCompatCommandNotInstalledTest {
companion object {
private const val TEST_PKG = "com.android.tests.gating.app_not_installed"
private const val TEST_CHANGE_ID = ParseInput.DeferredError.MISSING_APP_TAG
private val instrumentation = InstrumentationRegistry.getInstrumentation()
@JvmStatic
@BeforeClass
fun assumeDebuggable() {
assumeTrue(Build.IS_DEBUGGABLE)
}
@JvmStatic
@BeforeClass
fun assertNotInstalled() {
assertThrows(PackageManager.NameNotFoundException::class.java) {
instrumentation.context.packageManager
.getApplicationInfo(TEST_PKG, PackageManager.MATCH_ALL)
}
}
@JvmStatic
@Parameterized.Parameters(name = "{0}")
fun parameters() = arrayOf(
Params(enableDisable = null, targetSdk = 29, result = false),
Params(enableDisable = null, targetSdk = 30, result = true),
Params(enableDisable = true, targetSdk = 29, result = true),
Params(enableDisable = true, targetSdk = 30, result = true),
Params(enableDisable = false, targetSdk = 29, result = false),
Params(enableDisable = false, targetSdk = 30, result = false)
)
}
data class Params(val enableDisable: Boolean?, val targetSdk: Int, val result: Boolean)
@Parameterized.Parameter(0)
lateinit var params: Params
private val uiAutomation: UiAutomation = instrumentation.getUiAutomation()
private val platformCompat = IPlatformCompat.Stub.asInterface(
ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE))
@Before
fun resetChangeId() {
uiAutomation.adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
Manifest.permission.OVERRIDE_COMPAT_CHANGE_CONFIG,
Manifest.permission.READ_COMPAT_CHANGE_CONFIG)
val result = command("am compat reset $TEST_CHANGE_ID $TEST_PKG")
assertThat(result.startsWith("Reset change") || result.startsWith("No override"))
.isTrue()
}
fun ParcelFileDescriptor.text() = FileReader(fileDescriptor).readText()
@After
fun resetIdentity() = uiAutomation.dropShellPermissionIdentity()
@Test
fun execute() {
when (params.enableDisable) {
null -> { /* do nothing */
}
true -> assertThat(command("am compat enable $TEST_CHANGE_ID $TEST_PKG"))
.startsWith("Enabled change")
false -> assertThat(command("am compat disable $TEST_CHANGE_ID $TEST_PKG"))
.startsWith("Disabled change")
}
val appInfo = ApplicationInfo().apply {
this.packageName = TEST_PKG
this.targetSdkVersion = params.targetSdk
}
assertThat(platformCompat.isChangeEnabled(TEST_CHANGE_ID, appInfo)).isEqualTo(params.result)
}
private fun command(command: String) =
FileReader(uiAutomation.executeShellCommand(command).fileDescriptor).readText()
}