Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 38b94091 authored by Victor Hsieh's avatar Victor Hsieh
Browse files

Support measuring split

measurePackage is changed into measureApk, which would hint that the
method deals with a file instead of a "package" in the package manager's
term.

collectAppInfo is added to measure an app package, which may include
split.

Test
 * Add a test that leverages debug.transparency.bg-install-apps of BICs.
 * Copy BaseInstallMultiple from another test in frameworks/base to
   support split install.

Misc
 * Delete getApexInfo and corresponding unit tests, since it's now
   coverred through collectAllApexInfo in integration test. This also
   makes it easy for refactoring without having to keeping the Bundle
   works as the return type.

Bug: 264296226
Test: atest BinaryTransparencyServiceTest BinaryTransparencyHostTest
Change-Id: Iaa4118dfa8605acda313dbcc2466ae96a60b4721
parent 1f279326
Loading
Loading
Loading
Loading
+17 −18
Original line number Original line Diff line number Diff line
@@ -66,24 +66,6 @@ public class BinaryTransparencyManager {
        }
        }
    }
    }


    /**
     * Gets binary measurements of all installed APEXs, each packed in a Bundle.
     * @return A List of {@link android.os.Bundle}s with the following keys:
     *         {@link com.android.server.BinaryTransparencyService#BUNDLE_PACKAGE_INFO}
     *         {@link com.android.server.BinaryTransparencyService#BUNDLE_CONTENT_DIGEST_ALGORITHM}
     *         {@link com.android.server.BinaryTransparencyService#BUNDLE_CONTENT_DIGEST}
     */
    // TODO(b/259422958): Fix static constants referenced here - should be defined here
    @NonNull
    public List getApexInfo() {
        try {
            Slog.d(TAG, "Calling backend's getApexInfo()");
            return mService.getApexInfo();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
    /**
     * Collects the APEX information on the device.
     * Collects the APEX information on the device.
     *
     *
@@ -116,4 +98,21 @@ public class BinaryTransparencyManager {
            throw e.rethrowFromSystemServer();
            throw e.rethrowFromSystemServer();
        }
        }
    }
    }

    /**
     * Collects the silent installed MBA information on the device.
     *
     * @return A List containing the MBA info of silent installed.
     * @hide
     */
    @NonNull
    public List<IBinaryTransparencyService.AppInfo> collectAllSilentInstalledMbaInfo(
            Bundle packagesToSkip) {
        try {
            Slog.d(TAG, "Calling backend's collectAllSilentInstalledMbaInfo()");
            return mService.collectAllSilentInstalledMbaInfo(packagesToSkip);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }
}
}
+1 −2
Original line number Original line Diff line number Diff line
@@ -28,8 +28,6 @@ import android.os.Bundle;
interface IBinaryTransparencyService {
interface IBinaryTransparencyService {
    String getSignedImageInfo();
    String getSignedImageInfo();


    List getApexInfo();

    void recordMeasurementsForAllPackages();
    void recordMeasurementsForAllPackages();


    parcelable ApexInfo {
    parcelable ApexInfo {
@@ -60,4 +58,5 @@ interface IBinaryTransparencyService {
    /** Test only */
    /** Test only */
    List<ApexInfo> collectAllApexInfo(boolean includeTestOnly);
    List<ApexInfo> collectAllApexInfo(boolean includeTestOnly);
    List<AppInfo> collectAllUpdatedPreloadInfo(in Bundle packagesToSkip);
    List<AppInfo> collectAllUpdatedPreloadInfo(in Bundle packagesToSkip);
    List<AppInfo> collectAllSilentInstalledMbaInfo(in Bundle packagesToSkip);
}
}
 No newline at end of file
+96 −123
Original line number Original line Diff line number Diff line
@@ -34,6 +34,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentFilter;
import android.content.pm.ApexStagedEvent;
import android.content.pm.ApexStagedEvent;
import android.content.pm.ApplicationInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.Checksum;
import android.content.pm.IBackgroundInstallControlService;
import android.content.pm.IBackgroundInstallControlService;
import android.content.pm.IPackageManagerNative;
import android.content.pm.IPackageManagerNative;
import android.content.pm.IStagedApexObserver;
import android.content.pm.IStagedApexObserver;
@@ -84,6 +85,7 @@ import com.android.internal.os.IBinaryTransparencyService;
import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.FrameworkStatsLog;
import com.android.server.pm.ApexManager;
import com.android.server.pm.ApexManager;
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.pm.pkg.AndroidPackageSplit;
import com.android.server.pm.pkg.PackageState;
import com.android.server.pm.pkg.PackageState;


import libcore.util.HexEncoding;
import libcore.util.HexEncoding;
@@ -120,15 +122,6 @@ public class BinaryTransparencyService extends SystemService {


    static final long RECORD_MEASUREMENTS_COOLDOWN_MS = 24 * 60 * 60 * 1000;
    static final long RECORD_MEASUREMENTS_COOLDOWN_MS = 24 * 60 * 60 * 1000;


    @VisibleForTesting
    static final String BUNDLE_PACKAGE_NAME = "package-name";
    @VisibleForTesting
    static final String BUNDLE_PACKAGE_IS_APEX = "package-is-apex";
    @VisibleForTesting
    static final String BUNDLE_CONTENT_DIGEST_ALGORITHM = "content-digest-algo";
    @VisibleForTesting
    static final String BUNDLE_CONTENT_DIGEST = "content-digest";

    static final String APEX_PRELOAD_LOCATION_ERROR = "could-not-be-determined";
    static final String APEX_PRELOAD_LOCATION_ERROR = "could-not-be-determined";


    // used for indicating any type of error during MBA measurement
    // used for indicating any type of error during MBA measurement
@@ -170,29 +163,6 @@ public class BinaryTransparencyService extends SystemService {
            return mVbmetaDigest;
            return mVbmetaDigest;
        }
        }


        @Override
        public List getApexInfo() {
            List<Bundle> results = new ArrayList<>();

            for (PackageInfo packageInfo : getCurrentInstalledApexs()) {
                PackageState packageState = mPackageManagerInternal.getPackageStateInternal(
                        packageInfo.packageName);
                if (packageState == null) {
                    Slog.w(TAG, "Package state is unavailable, ignoring the package "
                            + packageInfo.packageName);
                    continue;
                }
                Bundle apexMeasurement = measurePackage(packageState);
                if (apexMeasurement == null) {
                    Slog.w(TAG, "Skipping the missing APEX in " + packageState.getPath());
                    continue;
                }
                results.add(apexMeasurement);
            }

            return results;
        }

        /**
        /**
         * A helper function to compute the SHA256 digest of APK package signer.
         * A helper function to compute the SHA256 digest of APK package signer.
         * @param signingInfo The signingInfo of a package, usually {@link PackageInfo#signingInfo}.
         * @param signingInfo The signingInfo of a package, usually {@link PackageInfo#signingInfo}.
@@ -217,58 +187,102 @@ public class BinaryTransparencyService extends SystemService {
            return resultList.toArray(new String[1]);
            return resultList.toArray(new String[1]);
        }
        }


        /**
        /*
         * Perform basic measurement (i.e. content digest) on a given package.
         * Perform basic measurement (i.e. content digest) on a given app, including the split APKs.
         *
         * @param packageState The package to be measured.
         * @param packageState The package to be measured.
         * @return a {@link android.os.Bundle} that packs the measurement result with the following
         * @param mbaStatus Assign this value of MBA status to the returned elements.
         *         keys: {@link #BUNDLE_PACKAGE_NAME},
         * @return a @{@code List<IBinaryTransparencyService.AppInfo>}
         *               {@link #BUNDLE_PACKAGE_IS_APEX}
         *               {@link #BUNDLE_CONTENT_DIGEST_ALGORITHM}
         *               {@link #BUNDLE_CONTENT_DIGEST}
         */
         */
        private @Nullable Bundle measurePackage(PackageState packageState) {
        private @NonNull List<IBinaryTransparencyService.AppInfo> collectAppInfo(
            Bundle result = new Bundle();
                PackageState packageState, int mbaStatus) {

            // compute content digest
            // compute content digest
            if (DEBUG) {
            if (DEBUG) {
                Slog.d(TAG, "Computing content digest for " + packageState.getPackageName() + " at "
                Slog.d(TAG, "Computing content digest for " + packageState.getPackageName() + " at "
                        + packageState.getPath());
                        + packageState.getPath());
            }
            }

            var results = new ArrayList<IBinaryTransparencyService.AppInfo>();

            // Same attributes across base and splits.
            String packageName = packageState.getPackageName();
            long versionCode = packageState.getVersionCode();
            String[] signerDigests =
                    computePackageSignerSha256Digests(packageState.getSigningInfo());

            AndroidPackage pkg = packageState.getAndroidPackage();
            AndroidPackage pkg = packageState.getAndroidPackage();
            if (pkg == null) {
            for (AndroidPackageSplit split : pkg.getSplits()) {
                Slog.w(TAG, "Skipping the missing APK in " + packageState.getPath());
                var appInfo = new IBinaryTransparencyService.AppInfo();
                return null;
                appInfo.packageName = packageName;
                appInfo.longVersion = versionCode;
                appInfo.splitName = split.getName();  // base's split name is null
                // Signer digests are consistent between splits, guaranteed by Package Manager.
                appInfo.signerDigests = signerDigests;
                appInfo.mbaStatus = mbaStatus;

                // Only digest and split name are different between splits.
                Checksum checksum = measureApk(split.getPath());
                appInfo.digest = checksum.getValue();
                appInfo.digestAlgorithm = checksum.getType();

                results.add(appInfo);
            }

            // InstallSourceInfo is only available per package name, so store it only on the base
            // APK. It's not current currently available in PackageState (there's a TODO), to we
            // need to extract manually with another call.
            //
            // Base APK is already the 0-th split from getSplits() and can't be null.
            AppInfo base = results.get(0);
            InstallSourceInfo installSourceInfo = getInstallSourceInfo(
                    packageState.getPackageName());
            if (installSourceInfo != null) {
                base.initiator = installSourceInfo.getInitiatingPackageName();
                SigningInfo initiatorSignerInfo =
                        installSourceInfo.getInitiatingPackageSigningInfo();
                if (initiatorSignerInfo != null) {
                    base.initiatorSignerDigests =
                        computePackageSignerSha256Digests(initiatorSignerInfo);
                }
                base.installer = installSourceInfo.getInstallingPackageName();
                base.originator = installSourceInfo.getOriginatingPackageName();
            }

            return results;
        }
        }
            Map<Integer, byte[]> contentDigests = computeApkContentDigest(pkg.getBaseApkPath());

            result.putString(BUNDLE_PACKAGE_NAME, pkg.getPackageName());
        /**
         * Perform basic measurement (i.e. content digest) on a given APK.
         *
         * @param apkPath The APK (or APEX, since it's also an APK) file to be measured.
         * @return a {@link android.content.pm.Checksum} with preferred digest algorithm type and
         *         the checksum.
         */
        private @Nullable Checksum measureApk(@NonNull String apkPath) {
            // compute content digest
            Map<Integer, byte[]> contentDigests = computeApkContentDigest(apkPath);
            if (contentDigests == null) {
            if (contentDigests == null) {
                Slog.d(TAG, "Failed to compute content digest for " + pkg.getBaseApkPath());
                Slog.d(TAG, "Failed to compute content digest for " + apkPath);
                result.putInt(BUNDLE_CONTENT_DIGEST_ALGORITHM, 0);
                return new Checksum(0, new byte[] { -1 });
                result.putByteArray(BUNDLE_CONTENT_DIGEST, null);
                return result;
            }
            }


            // in this iteration, we'll be supporting only 2 types of digests:
            // in this iteration, we'll be supporting only 2 types of digests:
            // CHUNKED_SHA256 and CHUNKED_SHA512.
            // CHUNKED_SHA256 and CHUNKED_SHA512.
            // And only one of them will be available per package.
            // And only one of them will be available per package.
            if (contentDigests.containsKey(ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256)) {
            if (contentDigests.containsKey(ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256)) {
                Integer algorithmId = ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256;
                return new Checksum(
                result.putInt(BUNDLE_CONTENT_DIGEST_ALGORITHM, algorithmId);
                        Checksum.TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256,
                result.putByteArray(BUNDLE_CONTENT_DIGEST, contentDigests.get(algorithmId));
                        contentDigests.get(ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256));
            } else if (contentDigests.containsKey(
            } else if (contentDigests.containsKey(
                    ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512)) {
                    ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512)) {
                Integer algorithmId = ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512;
                return new Checksum(
                result.putInt(BUNDLE_CONTENT_DIGEST_ALGORITHM, algorithmId);
                        Checksum.TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512,
                result.putByteArray(BUNDLE_CONTENT_DIGEST, contentDigests.get(algorithmId));
                        contentDigests.get(ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512));
            } else {
            } else {
                // TODO(b/259423111): considering putting the raw values for the algorithm & digest
                // TODO(b/259423111): considering putting the raw values for the algorithm & digest
                //  into the bundle to track potential other digest algorithms that may be in use
                //  into the bundle to track potential other digest algorithms that may be in use
                result.putInt(BUNDLE_CONTENT_DIGEST_ALGORITHM, 0);
                return new Checksum(0, new byte[] { -1 });
                result.putByteArray(BUNDLE_CONTENT_DIGEST, null);
            }
            }
            result.putBoolean(BUNDLE_PACKAGE_IS_APEX, packageState.isApex());

            return result;
        }
        }




@@ -330,7 +344,7 @@ public class BinaryTransparencyService extends SystemService {
            if (CompatChanges.isChangeEnabled(LOG_MBA_INFO)) {
            if (CompatChanges.isChangeEnabled(LOG_MBA_INFO)) {
                // lastly measure all newly installed MBAs
                // lastly measure all newly installed MBAs
                List<IBinaryTransparencyService.AppInfo> allMbaInfo =
                List<IBinaryTransparencyService.AppInfo> allMbaInfo =
                        collectAllMbaInfo(packagesMeasured);
                        collectAllSilentInstalledMbaInfo(packagesMeasured);
                for (IBinaryTransparencyService.AppInfo appInfo : allUpdatedPreloadInfo) {
                for (IBinaryTransparencyService.AppInfo appInfo : allUpdatedPreloadInfo) {
                    packagesMeasured.putBoolean(appInfo.packageName, true);
                    packagesMeasured.putBoolean(appInfo.packageName, true);
                    writeAppInfoToLog(appInfo);
                    writeAppInfoToLog(appInfo);
@@ -356,18 +370,22 @@ public class BinaryTransparencyService extends SystemService {
                    continue;
                    continue;
                }
                }


                Bundle apexMeasurement = measurePackage(packageState);
                AndroidPackage pkg = packageState.getAndroidPackage();
                if (apexMeasurement == null) {
                if (pkg == null) {
                    Slog.w(TAG, "Skipping the missing APEX in " + packageState.getPath());
                    Slog.w(TAG, "Skipping the missing APK in " + pkg.getPath());
                    continue;
                }
                Checksum apexChecksum = measureApk(pkg.getPath());
                if (apexChecksum == null) {
                    Slog.w(TAG, "Skipping the missing APEX in " + pkg.getPath());
                    continue;
                    continue;
                }
                }


                var apexInfo = new IBinaryTransparencyService.ApexInfo();
                var apexInfo = new IBinaryTransparencyService.ApexInfo();
                apexInfo.packageName = packageState.getPackageName();
                apexInfo.packageName = packageState.getPackageName();
                apexInfo.longVersion = packageState.getVersionCode();
                apexInfo.longVersion = packageState.getVersionCode();
                apexInfo.digest = apexMeasurement.getByteArray(BUNDLE_CONTENT_DIGEST);
                apexInfo.digest = apexChecksum.getValue();
                apexInfo.digestAlgorithm =
                apexInfo.digestAlgorithm = apexChecksum.getType();
                        apexMeasurement.getInt(BUNDLE_CONTENT_DIGEST_ALGORITHM);
                apexInfo.signerDigests =
                apexInfo.signerDigests =
                        computePackageSignerSha256Digests(packageState.getSigningInfo());
                        computePackageSignerSha256Digests(packageState.getSigningInfo());


@@ -398,28 +416,16 @@ public class BinaryTransparencyService extends SystemService {
                Slog.d(TAG, "Preload " + packageState.getPackageName() + " at "
                Slog.d(TAG, "Preload " + packageState.getPackageName() + " at "
                        + packageState.getPath() + " has likely been updated.");
                        + packageState.getPath() + " has likely been updated.");


                Bundle packageMeasurement = measurePackage(packageState);
                List<IBinaryTransparencyService.AppInfo> resultsForApp = collectAppInfo(
                if (packageMeasurement == null) {
                        packageState, MBA_STATUS_UPDATED_PRELOAD);
                    Slog.w(TAG, "Skipping the missing APK in " + packageState.getPath());
                results.addAll(resultsForApp);
                    return;
                }

                var appInfo = new IBinaryTransparencyService.AppInfo();
                appInfo.packageName = packageState.getPackageName();
                appInfo.longVersion = packageState.getVersionCode();
                appInfo.digest = packageMeasurement.getByteArray(BUNDLE_CONTENT_DIGEST);
                appInfo.digestAlgorithm =
                        packageMeasurement.getInt(BUNDLE_CONTENT_DIGEST_ALGORITHM);
                appInfo.signerDigests =
                        computePackageSignerSha256Digests(packageState.getSigningInfo());
                appInfo.mbaStatus = MBA_STATUS_UPDATED_PRELOAD;

                results.add(appInfo);
            });
            });
            return results;
            return results;
        }
        }


        public List<IBinaryTransparencyService.AppInfo> collectAllMbaInfo(Bundle packagesToSkip) {
        @Override
        public List<IBinaryTransparencyService.AppInfo> collectAllSilentInstalledMbaInfo(
                Bundle packagesToSkip) {
            var results = new ArrayList<IBinaryTransparencyService.AppInfo>();
            var results = new ArrayList<IBinaryTransparencyService.AppInfo>();
            for (PackageInfo packageInfo : getNewlyInstalledMbas()) {
            for (PackageInfo packageInfo : getNewlyInstalledMbas()) {
                if (packagesToSkip.containsKey(packageInfo.packageName)) {
                if (packagesToSkip.containsKey(packageInfo.packageName)) {
@@ -433,42 +439,9 @@ public class BinaryTransparencyService extends SystemService {
                    continue;
                    continue;
                }
                }


                Bundle packageMeasurement = measurePackage(packageState);
                List<IBinaryTransparencyService.AppInfo> resultsForApp = collectAppInfo(
                if (packageMeasurement == null) {
                        packageState, MBA_STATUS_NEW_INSTALL);
                    Slog.w(TAG, "Skipping the missing APK in " + packageState.getPath());
                results.addAll(resultsForApp);
                    continue;
                }
                if (DEBUG) {
                    Slog.d(TAG,
                            "Extracting InstallSourceInfo for " + packageState.getPackageName());
                }
                var appInfo = new IBinaryTransparencyService.AppInfo();
                appInfo.packageName = packageState.getPackageName();
                appInfo.longVersion = packageState.getVersionCode();
                appInfo.digest = packageMeasurement.getByteArray(BUNDLE_CONTENT_DIGEST);
                appInfo.digestAlgorithm =
                    packageMeasurement.getInt(BUNDLE_CONTENT_DIGEST_ALGORITHM);
                appInfo.signerDigests =
                        computePackageSignerSha256Digests(packageState.getSigningInfo());
                appInfo.mbaStatus = MBA_STATUS_NEW_INSTALL;

                // Install source isn't currently available in PackageState (there's a TODO).
                // Extract manually with another call.
                InstallSourceInfo installSourceInfo = getInstallSourceInfo(
                        packageState.getPackageName());
                if (installSourceInfo != null) {
                    appInfo.initiator = installSourceInfo.getInitiatingPackageName();
                    SigningInfo initiatorSignerInfo =
                            installSourceInfo.getInitiatingPackageSigningInfo();
                    if (initiatorSignerInfo != null) {
                        appInfo.initiatorSignerDigests =
                                computePackageSignerSha256Digests(initiatorSignerInfo);
                    }
                    appInfo.installer = installSourceInfo.getInstallingPackageName();
                    appInfo.originator = installSourceInfo.getOriginatingPackageName();
                }

                results.add(appInfo);
            }
            }
            return results;
            return results;
        }
        }
+0 −31
Original line number Original line Diff line number Diff line
@@ -40,7 +40,6 @@ import android.hardware.fingerprint.FingerprintManager;
import android.hardware.fingerprint.FingerprintSensorProperties;
import android.hardware.fingerprint.FingerprintSensorProperties;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback;
import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ResultReceiver;
import android.os.SystemProperties;
import android.os.SystemProperties;
@@ -165,36 +164,6 @@ public class BinaryTransparencyServiceTest {
                        BinaryTransparencyService.VBMETA_DIGEST_UNAVAILABLE), result);
                        BinaryTransparencyService.VBMETA_DIGEST_UNAVAILABLE), result);
    }
    }


    @Test
    public void getApexInfo_postInitialize_returnsValidEntries() throws RemoteException {
        prepApexInfo();
        List result = mTestInterface.getApexInfo();
        Assert.assertNotNull("Apex info map should not be null", result);
        // TODO(265244016): When PackageManagerInternal is a mock, it's harder to keep the
        // `measurePackage` working in unit test. Disable it for now. We may need more refactoring
        // or cover this in integration tests.
        // Assert.assertFalse("Apex info map should not be empty", result.isEmpty());
    }

    @Test
    public void getApexInfo_postInitialize_returnsActualApexs()
            throws RemoteException, PackageManager.NameNotFoundException {
        prepApexInfo();
        List resultList = mTestInterface.getApexInfo();

        PackageManager pm = mContext.getPackageManager();
        Assert.assertNotNull(pm);
        List<Bundle> castedResult = (List<Bundle>) resultList;
        for (Bundle resultBundle : castedResult) {
            String packageName = resultBundle.getString(
                    BinaryTransparencyService.BUNDLE_PACKAGE_NAME);
            Assert.assertNotNull("Package name for APEX should not be null", packageName);
            Assert.assertTrue(packageName + "is not an APEX!",
                    resultBundle.getBoolean(
                            BinaryTransparencyService.BUNDLE_PACKAGE_IS_APEX));
        }
    }

    @Test
    @Test
    public void testCollectBiometricProperties_disablesFeature() {
    public void testCollectBiometricProperties_disablesFeature() {
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_BIOMETRICS,
        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_BIOMETRICS,
+140 −0
Original line number Original line Diff line number Diff line
/*
 * 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.
 */
package android.transparency.test;

import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;

import junit.framework.TestCase;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Base class for invoking the install-multiple command via ADB. Subclass this for less typing:
 *
 * <code> private class InstallMultiple extends BaseInstallMultiple&lt;InstallMultiple&gt; { public
 * InstallMultiple() { super(getDevice(), null); } } </code>
 */
/*package*/ class BaseInstallMultiple<T extends BaseInstallMultiple<?>> {

    private final ITestDevice mDevice;
    private final IBuildInfo mBuild;

    private final List<String> mArgs = new ArrayList<>();
    private final Map<File, String> mFileToRemoteMap = new HashMap<>();

    /*package*/ BaseInstallMultiple(ITestDevice device, IBuildInfo buildInfo) {
        mDevice = device;
        mBuild = buildInfo;
        addArg("-g");
    }

    T addArg(String arg) {
        mArgs.add(arg);
        return (T) this;
    }

    T addFile(String filename) throws FileNotFoundException {
        return addFile(filename, filename);
    }

    T addFile(String filename, String remoteName) throws FileNotFoundException {
        CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mBuild);
        mFileToRemoteMap.put(buildHelper.getTestFile(filename), remoteName);
        return (T) this;
    }

    T inheritFrom(String packageName) {
        addArg("-r");
        addArg("-p " + packageName);
        return (T) this;
    }

    void run() throws DeviceNotAvailableException {
        run(true);
    }

    void runExpectingFailure() throws DeviceNotAvailableException {
        run(false);
    }

    private void run(boolean expectingSuccess) throws DeviceNotAvailableException {
        final ITestDevice device = mDevice;

        // Create an install session
        final StringBuilder cmd = new StringBuilder();
        cmd.append("pm install-create");
        for (String arg : mArgs) {
            cmd.append(' ').append(arg);
        }

        String result = device.executeShellCommand(cmd.toString());
        TestCase.assertTrue(result, result.startsWith("Success"));

        final int start = result.lastIndexOf("[");
        final int end = result.lastIndexOf("]");
        int sessionId = -1;
        try {
            if (start != -1 && end != -1 && start < end) {
                sessionId = Integer.parseInt(result.substring(start + 1, end));
            }
        } catch (NumberFormatException e) {
            throw new IllegalStateException("Failed to parse install session: " + result);
        }
        if (sessionId == -1) {
            throw new IllegalStateException("Failed to create install session: " + result);
        }

        // Push our files into session. Ideally we'd use stdin streaming,
        // but ddmlib doesn't support it yet.
        for (final Map.Entry<File, String> entry : mFileToRemoteMap.entrySet()) {
            final File file = entry.getKey();
            final String remoteName  = entry.getValue();
            final String remotePath = "/data/local/tmp/" + file.getName();
            if (!device.pushFile(file, remotePath)) {
                throw new IllegalStateException("Failed to push " + file);
            }

            cmd.setLength(0);
            cmd.append("pm install-write");
            cmd.append(' ').append(sessionId);
            cmd.append(' ').append(remoteName);
            cmd.append(' ').append(remotePath);

            result = device.executeShellCommand(cmd.toString());
            TestCase.assertTrue(result, result.startsWith("Success"));
        }

        // Everything staged; let's pull trigger
        cmd.setLength(0);
        cmd.append("pm install-commit");
        cmd.append(' ').append(sessionId);

        result = device.executeShellCommand(cmd.toString());
        if (expectingSuccess) {
            TestCase.assertTrue(result, result.contains("Success"));
        } else {
            TestCase.assertFalse(result, result.contains("Success"));
        }
    }
}
Loading