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

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

Merge "Create platform config parser and use it to check backup eligibility" into main

parents 48d2463d 85057038
Loading
Loading
Loading
Loading
+19 −1
Original line number Diff line number Diff line
@@ -298,7 +298,6 @@ public class FullBackup {
        }
    }

    @VisibleForTesting
    public static class BackupScheme {
        private final File FILES_DIR;
        private final File DATABASE_DIR;
@@ -451,6 +450,22 @@ public class FullBackup {
            public String getContentVersion() {
                return mContentVersion;
            }

            @Override
            public boolean equals(Object obj) {
                if (obj instanceof PlatformSpecificParams) {
                    PlatformSpecificParams other = (PlatformSpecificParams) obj;
                    return TextUtils.equals(mBundleId, other.mBundleId)
                            && TextUtils.equals(mTeamId, other.mTeamId)
                            && TextUtils.equals(mContentVersion, other.mContentVersion);
                }
                return false;
            }

            @Override
            public int hashCode() {
                return Objects.hash(mBundleId, mTeamId, mContentVersion);
            }
        }

        /**
@@ -598,6 +613,7 @@ public class FullBackup {
            // This not being null is how we know that we've tried to parse the xml already.
            mIncludes = new ArrayMap<String, Set<PathWithRequiredFlags>>();
            mExcludes = new ArraySet<PathWithRequiredFlags>();
            mPlatformSpecificParams = new ArrayMap<>();
            mRequiredTransportFlags = 0;
            mIsUsingNewScheme = false;

@@ -680,6 +696,8 @@ public class FullBackup {
                    return ConfigSection.CLOUD_BACKUP;
                case BackupDestination.DEVICE_TRANSFER:
                    return ConfigSection.DEVICE_TRANSFER;
                case BackupDestination.CROSS_PLATFORM_TRANSFER:
                    return ConfigSection.CROSS_PLATFORM_TRANSFER;
                default:
                    return null;
            }
+5 −0
Original line number Diff line number Diff line
@@ -3989,6 +3989,11 @@ public class UserBackupManagerService {
                            /* caller */ "BMS.getBackupDestinationFromTransport");
            if ((transport.getTransportFlags() & BackupAgent.FLAG_DEVICE_TO_DEVICE_TRANSFER) != 0) {
                return BackupDestination.DEVICE_TRANSFER;
            } else if (Flags.enableCrossPlatformTransfer()
                    && (transport.getTransportFlags()
                                    & BackupAgent.FLAG_CROSS_PLATFORM_DATA_TRANSFER_IOS)
                            != 0) {
                return BackupDestination.CROSS_PLATFORM_TRANSFER;
            } else {
                return BackupDestination.CLOUD;
            }
+203 −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.BackupManagerService.TAG;

import android.app.backup.FullBackup;
import android.app.backup.FullBackup.BackupScheme.PlatformSpecificParams;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
 * XML parser that extracts the cross platform configuration from a data extraction rules XML file.
 *
 * <p>This is similar to {@link FullBackup.BackupScheme}, but it does not require to be called
 * within an application context.
 *
 * @hide
 */
public class PlatformConfigParser {
    // Supported platforms
    public static final String PLATFORM_IOS = "ios";

    // XML sections and attributes
    private static final String DATA_EXTRACTION_RULES_SECTION = "data-extraction-rules";
    private static final String CROSS_PLATFORM_TRANSFER_SECTION = "cross-platform-transfer";
    private static final String PLATFORM_SPECIFIC_PARAMS_SECTION = "platform-specific-params";
    private static final String ATTR_PLATFORM = "platform";

    // Platform specific params for PLATFORM_IOS
    private static final String ATTR_BUNDLE_ID = "bundleId";
    private static final String ATTR_TEAM_ID = "teamId";
    private static final String ATTR_CONTENT_VERSION = "contentVersion";

    /**
     * Parses the platform-specific configuration from the data-extraction-rules of the given app.
     * If no data-extraction-rules are configured, an empty map is returned.
     *
     * @throws IOException when the package is not found or on malformed XML.
     */
    public static Map<String, List<PlatformSpecificParams>> parsePlatformSpecificConfig(
            PackageManager packageManager, ApplicationInfo applicationInfo) throws IOException {
        if (applicationInfo == null || applicationInfo.dataExtractionRulesRes == 0) {
            Slog.d(TAG, "No data extraction rules");
            return Collections.emptyMap();
        }
        int resourceId = applicationInfo.dataExtractionRulesRes;
        Resources resources;
        try {
            resources = packageManager.getResourcesForApplication(applicationInfo.packageName);
        } catch (PackageManager.NameNotFoundException e) {
            throw new IOException("Package not found", e);
        }
        try (XmlResourceParser xmlParser = resources.getXml(resourceId)) {
            return new PlatformConfigParser().parseConfig(xmlParser);
        } catch (XmlPullParserException e) {
            throw new IOException("Invalid XML", e);
        }
    }

    /**
     * Uses the {@link XmlPullParser} to parse the XML document and returns any valid platform
     * specific configuration for supported platforms. Unsupported platforms or any other unexpected
     * XML tags are ignored or skipped.
     *
     * @return A map containing the list of platform specific params for each platform.
     */
    @VisibleForTesting
    Map<String, List<PlatformSpecificParams>> parseConfig(XmlPullParser parser)
            throws IOException, XmlPullParserException {
        if (!hasExpectedRootTag(parser, DATA_EXTRACTION_RULES_SECTION)) {
            Slog.e(TAG, "Not a valid data-extraction-rules configuration.");
            return Collections.emptyMap();
        }
        return parsePlatformSpecificParams(parser);
    }

    /**
     * Checks whether the XML document starts with the given root tag.
     *
     * @return {@code true} if the first {@code START_TAG} of the {@link XmlPullParser} is equal to
     *     the given {@code tagName}, {@code false} otherwise.
     */
    private boolean hasExpectedRootTag(XmlPullParser parser, String tagName)
            throws IOException, XmlPullParserException {
        int event = parser.getEventType();
        while (event != XmlPullParser.START_TAG) {
            event = parser.next();
        }
        return TextUtils.equals(parser.getName(), tagName);
    }

    /**
     * Parses all {@code <cross-platform-transfer>} sections and the {@code
     * <platform-specific-params>} contained within. Ignores everything else.
     */
    private Map<String, List<PlatformSpecificParams>> parsePlatformSpecificParams(
            XmlPullParser parser) throws IOException, XmlPullParserException {
        Map<String, List<PlatformSpecificParams>> platformSpecificParamsPerPlatform =
                new ArrayMap<>();
        int event = parser.getEventType();
        while (event != XmlPullParser.END_DOCUMENT) {
            event = parser.next();

            if (event != XmlPullParser.START_TAG) {
                continue;
            }

            String tag = parser.getName();
            if (!TextUtils.equals(tag, CROSS_PLATFORM_TRANSFER_SECTION)) {
                skipSection(parser, tag);
                continue;
            }

            String platform = parser.getAttributeValue(/* namespace= */ null, ATTR_PLATFORM);
            if (!TextUtils.equals(platform, PLATFORM_IOS)) {
                Slog.w(
                        TAG,
                        "Cross-platform transfer does not support platform: \"" + platform + "\"");
                skipSection(parser, tag);
                continue;
            }

            List<PlatformSpecificParams> params = parseCrossPlatformTransferSection(parser);
            if (!params.isEmpty()) {
                platformSpecificParamsPerPlatform.put(platform, params);
            }
        }
        return platformSpecificParamsPerPlatform;
    }

    private void skipSection(XmlPullParser parser, String tag)
            throws IOException, XmlPullParserException {
        int event = parser.next();
        while (event != XmlPullParser.END_TAG && !TextUtils.equals(parser.getName(), tag)) {
            event = parser.next();
        }
    }

    private List<PlatformSpecificParams> parseCrossPlatformTransferSection(XmlPullParser parser)
            throws IOException, XmlPullParserException {
        List<PlatformSpecificParams> paramsList = new ArrayList<>();

        int event = parser.getEventType();
        while (!(event == XmlPullParser.END_TAG
                && TextUtils.equals(parser.getName(), CROSS_PLATFORM_TRANSFER_SECTION))) {
            event = parser.next();
            if (event != XmlPullParser.START_TAG) {
                continue;
            }

            String tag = parser.getName();
            if (!TextUtils.equals(tag, PLATFORM_SPECIFIC_PARAMS_SECTION)) {
                skipSection(parser, tag);
                continue;
            }

            String bundleId = parser.getAttributeValue(null, ATTR_BUNDLE_ID);
            String teamId = parser.getAttributeValue(null, ATTR_TEAM_ID);
            String contentVersion = parser.getAttributeValue(null, ATTR_CONTENT_VERSION);
            paramsList.add(new PlatformSpecificParams(bundleId, teamId, contentVersion));
            Slog.d(
                    TAG,
                    "Found platform specific params (bundleId=\""
                            + bundleId
                            + "\", teamId=\""
                            + teamId
                            + "\")");
        }

        return paramsList;
    }
}
+39 −0
Original line number Diff line number Diff line
@@ -22,11 +22,13 @@ import static com.android.server.backup.UserBackupManagerService.SETTINGS_PACKAG
import static com.android.server.backup.UserBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE;
import static com.android.server.backup.UserBackupManagerService.TELEPHONY_PROVIDER_PACKAGE;
import static com.android.server.backup.UserBackupManagerService.WALLPAPER_PACKAGE;
import static com.android.server.backup.crossplatform.PlatformConfigParser.PLATFORM_IOS;
import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;

import android.annotation.Nullable;
import android.app.backup.BackupAnnotations.BackupDestination;
import android.app.backup.BackupTransport;
import android.app.backup.FullBackup;
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
@@ -48,13 +50,18 @@ import com.android.internal.util.ArrayUtils;
import com.android.server.LocalServices;
import com.android.server.backup.BackupManagerService;
import com.android.server.backup.SetUtils;
import com.android.server.backup.crossplatform.PlatformConfigParser;
import com.android.server.backup.transport.BackupTransportClient;
import com.android.server.backup.transport.TransportConnection;
import com.android.server.pm.UserManagerInternal;

import com.google.android.collect.Sets;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

/** Utility methods wrapping operations on ApplicationInfo and PackageInfo. */
@@ -91,6 +98,9 @@ public class BackupEligibilityRules {
    @BackupDestination private final int mBackupDestination;
    private final boolean mSkipRestoreForLaunchedApps;

    private Map<String, List<FullBackup.BackupScheme.PlatformSpecificParams>>
            mPlatformSpecificParams;

    /**
     * When this change is enabled, {@code adb backup} is automatically turned on for apps running
     * as debuggable ({@code android:debuggable} set to {@code true}) and unavailable to any other
@@ -147,6 +157,7 @@ public class BackupEligibilityRules {
        mBackupDestination = backupDestination;
        mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
        mSkipRestoreForLaunchedApps = skipRestoreForLaunchedApps;
        mPlatformSpecificParams = Collections.emptyMap();
    }

    /**
@@ -296,6 +307,10 @@ public class BackupEligibilityRules {
                }
            case BackupDestination.CLOUD:
                return allowBackup;
            case BackupDestination.CROSS_PLATFORM_TRANSFER:
                return allowBackup
                        && (app.packageName.equals(PACKAGE_MANAGER_SENTINEL)
                                || appSupportsCrossPlatformTransfer(app, PLATFORM_IOS));
            default:
                Slog.w(TAG, "Unknown operation type:" + mBackupDestination);
                return false;
@@ -343,6 +358,30 @@ public class BackupEligibilityRules {
        }
    }

    /**
     * Returns whether an app has opted-in to cross-platform transfer in its data extraction rules.
     */
    public boolean appSupportsCrossPlatformTransfer(
            ApplicationInfo applicationInfo, String platform) {
        try {
            mPlatformSpecificParams =
                    PlatformConfigParser.parsePlatformSpecificConfig(
                            mPackageManager, applicationInfo);
        } catch (IOException e) {
            Slog.e(
                    TAG,
                    "Unable to parse cross-platform configuration from data-extraction-rules",
                    e);
            return false;
        }
        Slog.d(
                TAG,
                "Found cross-platform configuration for "
                        + mPlatformSpecificParams.size()
                        + " platforms.");
        return !mPlatformSpecificParams.getOrDefault(platform, Collections.emptyList()).isEmpty();
    }

    /**
     * Determine if data restore should be run for the given package.
     *
+160 −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.google.common.truth.Truth.assertThat;

import android.app.backup.FullBackup.BackupScheme.PlatformSpecificParams;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserFactory;

import java.io.StringReader;
import java.util.List;
import java.util.Map;

@RunWith(RobolectricTestRunner.class)
@Config
public class PlatformConfigParserTest {
    private XmlPullParserFactory mFactory;
    private XmlPullParser mXmlParser;
    private PlatformConfigParser mParser;

    @Before
    public void setUp() throws Exception {
        mFactory = XmlPullParserFactory.newInstance();
        mXmlParser = mFactory.newPullParser();
        mParser = new PlatformConfigParser();
    }

    @Test
    public void testValidConfig_success() throws Exception {
        mXmlParser.setInput(
                new StringReader(
                        "<data-extraction-rules><cloud-backup><include domain=\"file\""
                            + " path=\"backup\" /></cloud-backup><cross-platform-transfer"
                            + " platform=\"ios\"><exclude domain=\"database\" path=\".\""
                            + " /><platform-specific-params bundleId=\"bundle id\" teamId=\"team"
                            + " id\" contentVersion=\"1.0\" /></cross-platform-transfer>"
                            + "</data-extraction-rules>"));

        Map<String, List<PlatformSpecificParams>> result = mParser.parseConfig(mXmlParser);

        assertThat(result).hasSize(1);
        List<PlatformSpecificParams> iosParams = result.get("ios");
        assertThat(iosParams)
                .containsExactly(new PlatformSpecificParams("bundle id", "team id", "1.0"));
    }

    @Test
    public void testMultipleParams_returnsAll() throws Exception {
        mXmlParser.setInput(
                new StringReader(
                        "<data-extraction-rules><cross-platform-transfer"
                            + " platform=\"ios\"><platform-specific-params bundleId=\"bundle id\""
                            + " teamId=\"team id\" contentVersion=\"1.0\""
                            + " /><platform-specific-params bundleId=\"other bundle id\""
                            + " teamId=\"other team id\" contentVersion=\"1.1\" />"
                            + "</cross-platform-transfer></data-extraction-rules>"));

        Map<String, List<PlatformSpecificParams>> result = mParser.parseConfig(mXmlParser);

        assertThat(result).hasSize(1);
        List<PlatformSpecificParams> iosParams = result.get("ios");
        assertThat(iosParams)
                .containsExactly(
                        new PlatformSpecificParams("bundle id", "team id", "1.0"),
                        new PlatformSpecificParams("other bundle id", "other team id", "1.1"));
    }

    @Test
    public void testNoParams_empty() throws Exception {
        mXmlParser.setInput(
                new StringReader(
                        "<data-extraction-rules><cross-platform-transfer"
                                + " platform=\"ios\"></cross-platform-transfer>"
                                + "</data-extraction-rules>"));

        Map<String, List<PlatformSpecificParams>> result = mParser.parseConfig(mXmlParser);

        assertThat(result).isEmpty();
    }

    @Test
    public void testMissingConfig_empty() throws Exception {
        mXmlParser.setInput(new StringReader("<data-extraction-rules></data-extraction-rules>"));

        Map<String, List<PlatformSpecificParams>> result = mParser.parseConfig(mXmlParser);

        assertThat(result).isEmpty();
    }

    @Test
    public void testUnsupportedPlatform_ignored() throws Exception {
        mXmlParser.setInput(
                new StringReader(
                        "<data-extraction-rules><cross-platform-transfer"
                            + " platform=\"android\"><platform-specific-params bundleId=\"bundle"
                            + " id\" teamId=\"team id\" contentVersion=\"1.0\" />"
                            + "</cross-platform-transfer></data-extraction-rules>"));

        Map<String, List<PlatformSpecificParams>> result = mParser.parseConfig(mXmlParser);

        assertThat(result).isEmpty();
    }

    @Test
    public void testInvalidParentTag_ignored() throws Exception {
        mXmlParser.setInput(
                new StringReader(
                        "<data-extraction-rules><cloud-backup><platform-specific-params"
                            + " bundleId=\"bundle id\" teamId=\"team id\" contentVersion=\"1.0\" />"
                            + "</cloud-backup></data-extraction-rules>"));

        Map<String, List<PlatformSpecificParams>> result = mParser.parseConfig(mXmlParser);

        assertThat(result).isEmpty();
    }

    @Test
    public void testInvalidNesting_ignored() throws Exception {
        mXmlParser.setInput(
                new StringReader(
                        "<data-extraction-rules><cross-platform-transfer platform=\"ios\">"
                                + "<include><platform-specific-params bundleId=\"bundle id\""
                                + " teamId=\"team id\" contentVersion=\"1.0\" /></include>"
                                + "</cross-platform-transfer></data-extraction-rules>"));

        Map<String, List<PlatformSpecificParams>> result = mParser.parseConfig(mXmlParser);

        assertThat(result).isEmpty();
    }

    @Test
    public void testParseWrongDocument_empty() throws Exception {
        mXmlParser.setInput(new StringReader("<full-backup-content></full-backup-content>"));

        Map<String, List<PlatformSpecificParams>> result = mParser.parseConfig(mXmlParser);

        assertThat(result).isEmpty();
    }
}
Loading