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

Commit d3051712 authored by Joël Stemmer's avatar Joël Stemmer Committed by Android (Google) Code Review
Browse files

Merge changes I60b44358,I02019edc into main

* changes:
  Read and write the cross-platform manifest for cross-platform transfers
  Create reader and writer of the cross-platform manifest file
parents fff27394 06522151
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -189,6 +189,14 @@ public class UserBackupManagerService {
    public static final String BACKUP_MANIFEST_FILENAME = "_manifest";
    public static final int BACKUP_MANIFEST_VERSION = 1;

    // Name and current contents version of the cross-platform manifest file
    //
    // Manifest version history:
    //
    // 1 : initial release
    public static final String CROSS_PLATFORM_MANIFEST_FILENAME = "_cross_platform_manifest";
    public static final int CROSS_PLATFORM_MANIFEST_VERSION = 1;

    // External archive format version history:
    //
    // 1 : initial release
+130 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.backup.crossplatform;

import static com.android.server.backup.UserBackupManagerService.CROSS_PLATFORM_MANIFEST_VERSION;

import android.app.backup.FullBackup.BackupScheme.PlatformSpecificParams;
import android.content.pm.PackageInfo;
import android.util.StringBuilderPrinter;

import com.android.server.backup.BackupUtils;

import java.io.IOException;
import java.util.HexFormat;
import java.util.List;

/**
 * The cross-platform manifest contains metadata that is used during the transfer of data to and
 * from a non-Android platform.
 *
 * @hide
 */
public class CrossPlatformManifest {
    private final String mPackageName;
    private final String mPlatform;
    private final List<PlatformSpecificParams> mPlatformSpecificParams;
    private final List<byte[]> mSignatureHashes;

    /**
     * Create a new cross-platform manifest for the given package, platform and platform specific
     * parameters.
     */
    public static CrossPlatformManifest create(
            PackageInfo packageInfo,
            String platform,
            List<PlatformSpecificParams> platformSpecificParams) {
        return new CrossPlatformManifest(
                packageInfo.packageName,
                platform,
                platformSpecificParams,
                BackupUtils.hashSignatureArray(packageInfo.signingInfo.getApkContentsSigners()));
    }

    CrossPlatformManifest(
            String packageName,
            String platform,
            List<PlatformSpecificParams> platformSpecificParams,
            List<byte[]> signatureHashes) {
        mPackageName = packageName;
        mPlatform = platform;
        mPlatformSpecificParams = platformSpecificParams;
        mSignatureHashes = signatureHashes;
    }

    public String getPackageName() {
        return mPackageName;
    }

    public String getPlatform() {
        return mPlatform;
    }

    public List<PlatformSpecificParams> getPlatformSpecificParams() {
        return mPlatformSpecificParams;
    }

    public List<byte[]> getSignatureHashes() {
        return mSignatureHashes;
    }

    /**
     * Creates the app's manifest as a byte array. All data are strings ending in LF.
     *
     * <p>The manifest format is:
     *
     * <pre>
     *     CROSS_PLATFORM_MANIFEST_VERSION
     *     package name
     *     platform
     *     # of platform specific params
     *     N*
     *       bundle id
     *       team id
     *       content version
     *     # of signatures
     *       N* (hexadecimal representation of the signature hash)
     * </pre>
     */
    public byte[] toByteArray() throws IOException {
        StringBuilder builder = new StringBuilder(4096);
        StringBuilderPrinter printer = new StringBuilderPrinter(builder);

        printer.println(Integer.toString(CROSS_PLATFORM_MANIFEST_VERSION));
        printer.println(mPackageName);
        printer.println(mPlatform);

        printer.println(Integer.toString(mPlatformSpecificParams.size()));
        for (PlatformSpecificParams params : mPlatformSpecificParams) {
            printer.println(params.getBundleId());
            printer.println(params.getTeamId());
            printer.println(params.getContentVersion());
        }

        printer.println(Integer.toString(mSignatureHashes.size()));
        for (byte[] hash : mSignatureHashes) {
            printer.println(HexFormat.of().formatHex(hash));
        }

        return builder.toString().getBytes();
    }

    /** Parses the cross-platform manifest bytes created by {@link #toByteArray}. */
    public static CrossPlatformManifest parseFrom(byte[] manifestBytes) throws IOException {
        return CrossPlatformManifestParser.parse(manifestBytes);
    }
}
+150 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.backup.crossplatform;

import static com.android.server.backup.UserBackupManagerService.CROSS_PLATFORM_MANIFEST_VERSION;

import android.annotation.Nullable;
import android.app.backup.FullBackup.BackupScheme.PlatformSpecificParams;
import android.text.TextUtils;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HexFormat;
import java.util.List;

/**
 * Parses a cross-platform manifest file stored in a backup.
 *
 * @hide
 */
public class CrossPlatformManifestParser {
    /**
     * Takes the contents of a cross platform manifest file and returns a {@link
     * CrossPlatformManifest}.
     *
     * @throws IOException when encountering an unsupported version, incomplete manifest or any
     *     other errors during parsing.
     */
    static CrossPlatformManifest parse(byte[] manifestBytes) throws IOException {
        CrossPlatformManifestParser reader =
                new CrossPlatformManifestParser(new ByteArrayInputStream(manifestBytes));
        return reader.parse();
    }

    private final InputStream mInput;

    private CrossPlatformManifestParser(InputStream inputStream) {
        mInput = inputStream;
    }

    /**
     * Reads the app's cross-platform manifest from a byte stream.
     *
     * @see CrossPlatformManifest#toByteArray() for a description of the manifest format.
     */
    private CrossPlatformManifest parse() throws IOException {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(mInput))) {
            String version = reader.readLine();
            if (!TextUtils.equals(version, Integer.toString(CROSS_PLATFORM_MANIFEST_VERSION))) {
                throw new IOException("Unsupported cross-platform manifest version: " + version);
            }

            String packageName = reader.readLine();
            if (packageName == null) {
                throw new IOException("Incomplete cross-platform manifest: missing package");
            }

            String platform = reader.readLine();
            if (platform == null) {
                throw new IOException("Incomplete cross-platform manifest: missing platform");
            }

            Integer paramsSize = parseInt(reader.readLine());
            if (paramsSize == null) {
                throw new IOException("Incomplete cross-platform manifest: missing params");
            }

            List<PlatformSpecificParams> platformSpecificParams = Collections.emptyList();
            if (paramsSize > 0) {
                platformSpecificParams = parsePlatformSpecificParams(reader, paramsSize);
            }

            Integer hashesSize = parseInt(reader.readLine());
            if (hashesSize == null) {
                throw new IOException("Incomplete cross-platform manifest: missing signatures");
            }

            List<byte[]> signatureHashes = Collections.emptyList();
            if (hashesSize > 0) {
                signatureHashes = parseSignatureHashes(reader, hashesSize);
            }

            return new CrossPlatformManifest(
                            packageName, platform, platformSpecificParams, signatureHashes);
        }
    }

    private List<PlatformSpecificParams> parsePlatformSpecificParams(
            BufferedReader reader, int numParams) throws IOException {
        ArrayList<PlatformSpecificParams> params = new ArrayList<>(numParams);
        for (int i = 0; i < numParams; i++) {
            String bundleId = reader.readLine();
            if (bundleId == null) {
                throw new IOException(
                        "Incomplete cross-platform manifest: missing bundle id parameter");
            }
            String teamId = reader.readLine();
            if (teamId == null) {
                throw new IOException(
                        "Incomplete cross-platform manifest: missing team id parameter");
            }
            String contentVersion = reader.readLine();
            if (contentVersion == null) {
                throw new IOException(
                        "Incomplete cross-platform manifest: missing content version parameter");
            }
            params.add(new PlatformSpecificParams(bundleId, teamId, contentVersion));
        }
        return params;
    }

    private List<byte[]> parseSignatureHashes(BufferedReader reader, int numSignatureHashes)
            throws IOException {
        ArrayList<byte[]> hashes = new ArrayList<>(numSignatureHashes);
        for (int i = 0; i < numSignatureHashes; i++) {
            String hash = reader.readLine();
            if (hash == null) {
                throw new IOException("Incomplete cross-platform manifest: missing signature");
            }
            hashes.add(HexFormat.of().parseHex(hash));
        }
        return hashes;
    }

    private static Integer parseInt(@Nullable String number) {
        if (number == null) {
            return null;
        }
        return Integer.parseInt(number);
    }
}
+42 −0
Original line number Diff line number Diff line
@@ -20,13 +20,17 @@ import static com.android.server.backup.BackupManagerService.DEBUG;
import static com.android.server.backup.BackupManagerService.TAG;
import static com.android.server.backup.UserBackupManagerService.BACKUP_MANIFEST_FILENAME;
import static com.android.server.backup.UserBackupManagerService.BACKUP_METADATA_FILENAME;
import static com.android.server.backup.UserBackupManagerService.CROSS_PLATFORM_MANIFEST_FILENAME;
import static com.android.server.backup.UserBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE;
import static com.android.server.backup.crossplatform.PlatformConfigParser.PLATFORM_IOS;

import android.annotation.UserIdInt;
import android.app.ApplicationThreadConstants;
import android.app.IBackupAgent;
import android.app.backup.BackupAgent;
import android.app.backup.BackupManagerMonitor;
import android.app.backup.BackupTransport;
import android.app.backup.FullBackup;
import android.app.backup.FullBackupDataOutput;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
@@ -38,14 +42,17 @@ import android.util.Slog;
import com.android.server.AppWidgetBackupBridge;
import com.android.server.backup.BackupAgentTimeoutParameters;
import com.android.server.backup.BackupRestoreTask;
import com.android.server.backup.Flags;
import com.android.server.backup.OperationStorage.OpType;
import com.android.server.backup.UserBackupManagerService;
import com.android.server.backup.crossplatform.CrossPlatformManifest;
import com.android.server.backup.remote.RemoteCall;
import com.android.server.backup.utils.BackupEligibilityRules;
import com.android.server.backup.utils.BackupManagerMonitorEventSender;
import com.android.server.backup.utils.FullBackupUtils;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Objects;
@@ -137,6 +144,12 @@ public class FullBackupEngine {
                    appMetadataBackupWriter.backupObb(mUserId, mPackage);
                }

                if (Flags.enableCrossPlatformTransfer()
                        && (mTransportFlags & BackupAgent.FLAG_CROSS_PLATFORM_DATA_TRANSFER_IOS)
                                != 0) {
                    backupCrossPlatformManifest(output, mPackage.applicationInfo);
                }

                Slog.d(TAG, "Calling doFullBackup() on " + packageName);

                long timeout =
@@ -181,6 +194,35 @@ public class FullBackupEngine {
                    && !isSharedStorage
                    && (!isSystemApp || isUpdatedSystemApp);
        }

        /** Back up the app's cross platform manifest. */
        private void backupCrossPlatformManifest(
                FullBackupDataOutput output, ApplicationInfo applicationInfo) throws IOException {
            CrossPlatformManifest manifest =
                    CrossPlatformManifest.create(
                            mPackage,
                            PLATFORM_IOS,
                            mBackupEligibilityRules.getPlatformSpecificParams(
                                    applicationInfo, PLATFORM_IOS));
            File manifestFile = new File(mFilesDir, CROSS_PLATFORM_MANIFEST_FILENAME);
            try (FileOutputStream out = new FileOutputStream(manifestFile)) {
                out.write(manifest.toByteArray());
            }

            // We want the manifest block in the archive stream to be constant each time we generate
            // a backup stream for the app. However, the underlying TAR mechanism sees it as a file
            // and will propagate its last modified time. We pin the last modified time to zero to
            // prevent the TAR header from varying.
            manifestFile.setLastModified(0);

            FullBackup.backupToTar(
                    mPackage.packageName,
                    /* domain= */ null,
                    /* linkdomain= */ null,
                    mFilesDir.getAbsolutePath(),
                    manifestFile.getAbsolutePath(),
                    output);
        }
    }

    public FullBackupEngine(
+83 −5
Original line number Diff line number Diff line
@@ -20,10 +20,13 @@ import static com.android.server.backup.BackupManagerService.DEBUG;
import static com.android.server.backup.BackupManagerService.TAG;
import static com.android.server.backup.UserBackupManagerService.BACKUP_MANIFEST_FILENAME;
import static com.android.server.backup.UserBackupManagerService.BACKUP_METADATA_FILENAME;
import static com.android.server.backup.UserBackupManagerService.CROSS_PLATFORM_MANIFEST_FILENAME;
import static com.android.server.backup.UserBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE;
import static com.android.server.backup.crossplatform.PlatformConfigParser.PLATFORM_IOS;
import static com.android.server.backup.internal.BackupHandler.MSG_RESTORE_OPERATION_TIMEOUT;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ApplicationThreadConstants;
import android.app.IBackupAgent;
import android.app.backup.BackupAgent;
@@ -31,10 +34,12 @@ import android.app.backup.BackupAnnotations;
import android.app.backup.BackupAnnotations.BackupDestination;
import android.app.backup.BackupManagerMonitor;
import android.app.backup.FullBackup;
import android.app.backup.FullBackup.BackupScheme.PlatformSpecificParams;
import android.app.backup.IBackupManagerMonitor;
import android.app.backup.IFullBackupRestoreObserver;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PackageManagerInternal;
import android.content.pm.Signature;
@@ -56,6 +61,7 @@ import com.android.server.backup.KeyValueAdbRestoreEngine;
import com.android.server.backup.OperationStorage;
import com.android.server.backup.OperationStorage.OpType;
import com.android.server.backup.UserBackupManagerService;
import com.android.server.backup.crossplatform.CrossPlatformManifest;
import com.android.server.backup.fullbackup.FullBackupObbConnection;
import com.android.server.backup.utils.BackupEligibilityRules;
import com.android.server.backup.utils.BackupManagerMonitorEventSender;
@@ -129,7 +135,10 @@ public class FullRestoreEngine extends RestoreEngine {

    // Widget blob to be restored out-of-band
    private byte[] mWidgetData = null;

    private int mTransportFlags = 0;
    private long mAppVersion;
    private String mContentVersion = "";

    final int mEphemeralOpToken;

@@ -182,7 +191,9 @@ public class FullRestoreEngine extends RestoreEngine {
    }

    @VisibleForTesting
    FullRestoreEngine() {
    FullRestoreEngine(UserBackupManagerService backupManagerService) {
        mBackupManagerService = backupManagerService;

        mIsAdbRestore = false;
        mAllowApks = false;
        mEphemeralOpToken = 0;
@@ -190,7 +201,6 @@ public class FullRestoreEngine extends RestoreEngine {
        mBackupEligibilityRules = null;
        mAgentTimeoutParameters = null;
        mBuffer = null;
        mBackupManagerService = null;
        mOperationStorage = null;
        mMonitor = null;
        mMonitorTask = null;
@@ -290,6 +300,37 @@ public class FullRestoreEngine extends RestoreEngine {
                    mManifestSignatures.put(info.packageName, signatures);
                    mPackagePolicies.put(pkg, restorePolicy);
                    mPackageInstallers.put(pkg, info.installerPackageName);
                    // We've read only the manifest content itself at this point,
                    // so consume the footer before looping around to the next
                    // input file
                    tarBackupReader.skipTarPadding(info.size);
                    mObserver = FullBackupRestoreObserverUtils.sendOnRestorePackage(mObserver, pkg);
                } else if (Flags.enableCrossPlatformTransfer()
                        && info.path.equals(CROSS_PLATFORM_MANIFEST_FILENAME)) {
                    // We start by reading the manifest content so we consume the data before doing
                    // any further checks.
                    CrossPlatformManifest manifest =
                            tarBackupReader.readCrossPlatformManifest(info);
                    if (mBackupEligibilityRules.getBackupDestination()
                            != BackupDestination.CROSS_PLATFORM_TRANSFER) {
                        mPackagePolicies.put(pkg, RestorePolicy.IGNORE);
                    } else {
                        PlatformSpecificParams params =
                                findValidPlatformSpecificParams(
                                        info.packageName, manifest, mBackupEligibilityRules);
                        if (params == null) {
                            Slog.w(
                                    TAG,
                                    "No source declared platform-specific params found that match"
                                            + " the target app");
                            mPackagePolicies.put(pkg, RestorePolicy.IGNORE);
                        } else {
                            mPackagePolicies.put(pkg, RestorePolicy.ACCEPT);
                            mTransportFlags |= BackupAgent.FLAG_CROSS_PLATFORM_DATA_TRANSFER_IOS;
                            mContentVersion = params.getContentVersion();
                        }
                    }

                    // We've read only the manifest content itself at this point,
                    // so consume the footer before looping around to the next
                    // input file
@@ -541,9 +582,9 @@ public class FullRestoreEngine extends RestoreEngine {
                                            info.mtime,
                                            token,
                                            mBackupManagerService.getBackupManagerBinder(),
                                            /* appVersionCode= */ 0,
                                            /* transportFlags= */ 0,
                                            /* contentVersion= */ "");
                                            mAppVersion,
                                            mTransportFlags,
                                            mContentVersion);
                                }
                            }
                        } catch (IOException e) {
@@ -879,4 +920,41 @@ public class FullRestoreEngine extends RestoreEngine {
                                : ApplicationThreadConstants.BACKUP_MODE_RESTORE_FULL,
                        mBackupEligibilityRules.getBackupDestination());
    }

    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    PlatformSpecificParams findValidPlatformSpecificParams(
            String pkg,
            @Nullable CrossPlatformManifest manifest,
            BackupEligibilityRules backupEligibilityRules) {
        if (manifest == null) {
            Slog.d(TAG, "No cross-platform manifest found.");
            return null;
        }

        ApplicationInfo targetApp;
        try {
            targetApp = mBackupManagerService
                    .getPackageManager()
                    .getApplicationInfoAsUser(
                            pkg,
                            PackageManager.GET_SIGNING_CERTIFICATES,
                            mUserId);
        } catch (NameNotFoundException e) {
            Slog.d(TAG, "Unable to fetch cross-platform configuration for target app", e);
            return null;
        }

        // Both source and target may specify multiple platform specific params. For us to continue
        // restoring, there should be at least one combination that matches.
        for (PlatformSpecificParams sourceParams : manifest.getPlatformSpecificParams()) {
            for (PlatformSpecificParams targetParams :
                    backupEligibilityRules.getPlatformSpecificParams(targetApp, PLATFORM_IOS)) {
                if (TextUtils.equals(sourceParams.getBundleId(), targetParams.getBundleId())
                        && TextUtils.equals(sourceParams.getTeamId(), targetParams.getTeamId())) {
                    return sourceParams;
                }
            }
        }
        return null;
    }
}
Loading