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

Commit fdb9ac7d authored by Mina Granic's avatar Mina Granic
Browse files

Backup and Restore User Aspect Ratio Settings.

Flag: com.android.window.flags.backup_and_restore_for_user_aspect_ratio_settings
Bug: 405901435
Test: atest UserAspectRatioBackupManageTest
Test: manual
Change-Id: I95c19d47818927482933c53cde5996582c73756a
parent b18aaa49
Loading
Loading
Loading
Loading
+88 −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.settings.applications.appcompat;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.backup.BackupRestoreEventLogger;
import android.app.backup.BlobBackupHelper;
import android.content.Context;
import android.content.pm.IPackageManager;
import android.util.Slog;

/** A {@link BlobBackupHelper} that handles backup and restore of user aspect ratio settings.*/
public class UserAspectRatioBackupHelper extends BlobBackupHelper {
    private static final String TAG = "UsrAspRatioBackupHlp"; // Must be < 23 characters.
    private static final boolean DEBUG = false;

    @BackupRestoreEventLogger.BackupRestoreDataType
    private static final String DATA_TYPE_USER_ASPECT_RATIO = "user_aspect_ratio:user_aspect_ratio";
    @BackupRestoreEventLogger.BackupRestoreError
    private static final String ERROR_UNEXPECTED_KEY = "unexpected_key";

    // Key under which the payload blob is stored
    public static final String KEY_USER_ASPECT_RATIO = "user_aspect_ratio";

    // Current version of the blob schema
    private static final int BLOB_VERSION = 1;

    private final UserAspectRatioBackupManager mUserAspectRatioBackupManager;

    public UserAspectRatioBackupHelper(@NonNull Context context,
            @NonNull IPackageManager packageManager, @NonNull BackupRestoreEventLogger logger) {
        super(BLOB_VERSION, KEY_USER_ASPECT_RATIO);
        mUserAspectRatioBackupManager = new UserAspectRatioBackupManager(context, packageManager,
                context.getPackageManager());
        setLogger(logger);
    }

    @Override
    @Nullable
    protected byte[] getBackupPayload(@NonNull String key) {
        if (DEBUG) {
            Slog.d(TAG, "Handling backup of " + key);
        }
        byte[] newPayload = null;
        if (KEY_USER_ASPECT_RATIO.equals(key)) {
            newPayload = mUserAspectRatioBackupManager.getBackupPayload(getLogger());
            getLogger().logItemsBackedUp(DATA_TYPE_USER_ASPECT_RATIO, /* count= */ 1);
        } else {
            Slog.w(TAG, "Unexpected backup key " + key);
            getLogger().logItemsBackupFailed(DATA_TYPE_USER_ASPECT_RATIO, /* count= */ 1,
                    ERROR_UNEXPECTED_KEY);
        }
        return newPayload;
    }

    @Override
    protected void applyRestoredPayload(@NonNull String key, @Nullable byte[] payload) {
        if (DEBUG) {
            Slog.d(TAG, "Handling restore of " + key);
        }
        if (KEY_USER_ASPECT_RATIO.equals(key)) {
            if (payload == null) {
                return;
            }
            mUserAspectRatioBackupManager.stageAndApplyRestoredPayload(payload, getLogger());
            getLogger().logItemsRestored(DATA_TYPE_USER_ASPECT_RATIO, /* count= */ 1);
        } else {
            Slog.w(TAG, "Unexpected restore key " + key);
            getLogger().logItemsRestoreFailed(DATA_TYPE_USER_ASPECT_RATIO, /* count= */ 1,
                    ERROR_UNEXPECTED_KEY);
        }
    }
}
+245 −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.settings.applications.appcompat;

import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_APP_DEFAULT;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_UNSET;

import static com.android.settings.applications.appcompat.UserAspectRatioBackupHelper.KEY_USER_ASPECT_RATIO;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.backup.BackupRestoreEventLogger;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.os.RemoteException;
import android.util.Slog;

import com.android.settings.R;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Manager class for performing Backup & Restore for per-app user aspect ratio override.
 *
 * @hide
 */
public class UserAspectRatioBackupManager {
    private static final String TAG = "UserAspRatioBackupMngr";   // must be < 23 chars
    private static final boolean DEBUG = false;

    @BackupRestoreEventLogger.BackupRestoreError
    private static final String ERROR_SERIALIZE_FAILED = "serialize_failed";
    @BackupRestoreEventLogger.BackupRestoreError
    private static final String ERROR_QUERY_ASPECT_RATIO_FAILED =
            "backup_query_packages_failed";
    @BackupRestoreEventLogger.BackupRestoreError
    private static final String ERROR_DESERIALIZE_FAILED = "deserialize_failed";
    @BackupRestoreEventLogger.BackupRestoreError
    private static final String ERROR_TYPE_CAST_FAILED = "type_cast_failed";
    @BackupRestoreEventLogger.BackupRestoreError
    private static final String ERROR_QUERY_PACKAGE_FAILED = "query_package_failed";
    @BackupRestoreEventLogger.BackupRestoreError
    private static final String ERROR_ASPECT_RATIO_FAILED = "aspect_ratio_failed";

    @NonNull
    private final IPackageManager mIPackageManager;
    @NonNull
    private final PackageManager mPackageManager;
    @NonNull
    private final Set<Integer> mAvailableUserMinAspectRatioSet;

    @UserIdInt
    private final int mUserId;

    public UserAspectRatioBackupManager(@NonNull Context context,
            @NonNull IPackageManager iPackageManager, @NonNull PackageManager packageManager) {
        mIPackageManager = iPackageManager;
        mPackageManager = packageManager;
        mUserId = mPackageManager.getUserId();

        final int[] userAspectRatioResourceValues = context.getResources().getIntArray(
                R.array.config_userAspectRatioOverrideValues);
        mAvailableUserMinAspectRatioSet = Arrays.stream(
                userAspectRatioResourceValues).boxed().collect(Collectors.toSet());
        // App Default is not in the set above, but is always offered. Users can also specify this
        // value, for example if there is an OEM override to some other value.
        mAvailableUserMinAspectRatioSet.add(USER_MIN_ASPECT_RATIO_APP_DEFAULT);
    }

    /**
     * Returns the per-app user aspect ratio settings to be backed up as a data-blob.
     */
    @Nullable
    public byte[] getBackupPayload(@NonNull BackupRestoreEventLogger logger) {
        final Map<String, Integer> aspectRatioStates = getAllUserAspectRatios(logger);

        if (DEBUG) {
            Slog.d(TAG, "User aspect ratio states to backup =" + aspectRatioStates);
        }

        try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos =
                new ObjectOutputStream(bos)) {
            oos.writeObject(aspectRatioStates);
            return bos.toByteArray();
        } catch (IOException e) {
            logger.logItemsBackupFailed(KEY_USER_ASPECT_RATIO, /* count= */ 1,
                    ERROR_SERIALIZE_FAILED);
            Slog.e(TAG, "Could not serialize payload.", e);
            return null;
        }
    }

    @NonNull
    private Map<String, Integer> getAllUserAspectRatios(@NonNull BackupRestoreEventLogger logger) {
        final List<ApplicationInfo> appList = mPackageManager.getInstalledApplications(
                PackageManager.MATCH_DISABLED_COMPONENTS
                        | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS);

        final HashMap<String, Integer> aspectRatioStates = new HashMap<>();
        for (ApplicationInfo app : appList) {
            try {
                final int aspectRatio = mIPackageManager.getUserMinAspectRatio(app.packageName,
                        mUserId);
                if (aspectRatio != USER_MIN_ASPECT_RATIO_UNSET) {
                    aspectRatioStates.put(app.packageName, aspectRatio);
                }
            } catch (RemoteException e) {
                logger.logItemsBackupFailed(KEY_USER_ASPECT_RATIO, /* count= */ 1,
                        ERROR_QUERY_ASPECT_RATIO_FAILED);
                Slog.e(TAG, "Could not get user aspect ratio for " + app.packageName + ".", e);
            }
        }
        return aspectRatioStates;
    }

    @Nullable
    private static Map<String, Integer> readFromByteArray(@NonNull byte[] payload,
            @NonNull BackupRestoreEventLogger logger) {
        Object readObject = null;
        try (ByteArrayInputStream bis = new ByteArrayInputStream(payload); ObjectInputStream ois =
                new ObjectInputStream(bis)) {
            readObject = ois.readObject();
            // Cannot check generic type, therefore ClassCastException will be thrown and logged if
            // type is incorrect.
            return (Map<String, Integer>) readObject;
        } catch (ClassCastException e) {
            logger.logItemsRestoreFailed(KEY_USER_ASPECT_RATIO, /* count= */ 1,
                    ERROR_TYPE_CAST_FAILED);
            Slog.e(TAG, "Could not cast to Map<String, Integer>: " + readObject, e);
            return null;
        } catch (IOException | ClassNotFoundException e) {
            logger.logItemsRestoreFailed(KEY_USER_ASPECT_RATIO, /* count= */ 1,
                    ERROR_DESERIALIZE_FAILED);
            Slog.e(TAG, "Could not read payload for backup", e);
            return null;
        }
    }

    /**
     * Restores the per-app user aspect ratio settings that were previously backed up.
     *
     * <p>This method will parse the input data blob and restore the aspect ratio settings for apps
     * which are present on the device. It will stage the aspect ratio data for the apps which are
     * not installed at the time this is called, to be referenced later when the app is installed.
     */
    public void stageAndApplyRestoredPayload(@NonNull byte[] payload,
            @NonNull BackupRestoreEventLogger logger) {
        final Map<String, Integer> userAspectRatioStates = readFromByteArray(payload, logger);
        if (userAspectRatioStates == null || userAspectRatioStates.isEmpty()) {
            Slog.d(TAG, "StageAndApplyRestoredPayload: payload is empty.");
            return;
        }
        if (DEBUG) {
            Slog.d(TAG, "StageAndApplyRestoredPayload user=" + mUserId + " payload="
                    + new String(payload, StandardCharsets.UTF_8));
        }

        for (String pkgName : userAspectRatioStates.keySet()) {
            final Integer aspectRatio = userAspectRatioStates.get(pkgName);
            if (aspectRatio == null) {
                Slog.d(TAG, "StageAndApplyRestoredPayload null aspect ratio for package: "
                        + pkgName);
                continue;
            }
            if (isPackageInstalled(pkgName, logger)) {
                Slog.d(TAG, "StageAndApplyRestoredPayload Found package: " + pkgName);
                checkExistingAspectRatioAndApplyRestore(pkgName, aspectRatio, logger);
            } else {
                // TODO(b/405902444): Support lazy restore when package is installed.
                Slog.d(TAG, "StageAndApplyRestoredPayload package not installed: " + pkgName);
            }
        }
    }

    private boolean isPackageInstalled(String packageName,
            @NonNull BackupRestoreEventLogger logger) {
        try {
            return mPackageManager.getPackageInfo(packageName, /* flags= */ 0) != null;
        } catch (PackageManager.NameNotFoundException e) {
            logger.logItemsRestoreFailed(KEY_USER_ASPECT_RATIO, /* count= */ 1,
                    ERROR_QUERY_PACKAGE_FAILED);
            if (DEBUG) {
                Slog.d(TAG, "Could not get package info for " + packageName, e);
            }
            return false;
        }
    }

    /** Applies the restore for per-app user set min aspect ratio. */
    private void checkExistingAspectRatioAndApplyRestore(@NonNull String pkgName,
            @PackageManager.UserMinAspectRatio int aspectRatio,
            @NonNull BackupRestoreEventLogger logger) {
        try {
            final int existingUserAspectRatio = mIPackageManager.getUserMinAspectRatio(pkgName,
                    mUserId);
            // Don't apply the restore if the aspect ratio have already been set for the app.
            if (existingUserAspectRatio != USER_MIN_ASPECT_RATIO_UNSET) {
                Slog.d(TAG, "Not restoring user aspect ratio=" + aspectRatio + " for package="
                        + pkgName + " as it is already set to " + existingUserAspectRatio + ".");
                return;
            }
            // TODO(b/407738654): Implement: when an aspect ratio option is unavailable, fallback to
            //  the closest aspect ratio option that results in bigger app bounds.
            if (mAvailableUserMinAspectRatioSet.contains(aspectRatio)) {
                mIPackageManager.setUserMinAspectRatio(pkgName, mUserId, aspectRatio);
                if (DEBUG) {
                    Slog.d(TAG, "Restored user aspect ratio=" + aspectRatio + " for package="
                            + pkgName);
                }
            }
        } catch (RemoteException | IllegalArgumentException e) {
            logger.logItemsRestoreFailed(KEY_USER_ASPECT_RATIO, /* count= */ 1,
                    ERROR_ASPECT_RATIO_FAILED);
            Slog.e(TAG, "Could not restore user aspect ratio for package " + pkgName, e);
        }
    }
}
+14 −5
Original line number Diff line number Diff line
@@ -35,6 +35,7 @@ import static java.lang.Boolean.FALSE;

import android.app.ActivityTaskManager;
import android.app.AppGlobals;
import android.app.backup.BackupManager;
import android.app.compat.CompatChanges;
import android.content.Context;
import android.content.pm.ApplicationInfo;
@@ -53,6 +54,7 @@ import androidx.annotation.Nullable;

import com.android.settings.R;
import com.android.settings.Utils;
import com.android.window.flags.Flags;

import com.google.common.annotations.VisibleForTesting;

@@ -80,14 +82,16 @@ public class UserAspectRatioManager {
    private final Map<Integer, CharSequence> mUserAspectRatioA11yMap;
    private final SparseIntArray mUserAspectRatioOrder;
    private final ActivityTaskManager mActivityTaskManager;
    private final BackupManager mBackupManager;

    public UserAspectRatioManager(@NonNull Context context) {
        this(context, AppGlobals.getPackageManager());
        this(context, AppGlobals.getPackageManager(), new BackupManager(context));
    }

    @VisibleForTesting
    UserAspectRatioManager(@NonNull Context context, @NonNull IPackageManager pm,
                           @NonNull ActivityTaskManager activityTaskManager) {
            @NonNull ActivityTaskManager activityTaskManager,
            @NonNull BackupManager backupManager) {
        mContext = context;
        mIPm = pm;
        mUserAspectRatioA11yMap = new ArrayMap<>();
@@ -96,11 +100,12 @@ public class UserAspectRatioManager {
        mIgnoreActivityOrientationRequest = getValueFromDeviceConfig(
                "ignore_activity_orientation_request", false);
        mActivityTaskManager = activityTaskManager;

        mBackupManager = backupManager;
    }

    UserAspectRatioManager(@NonNull Context context, @NonNull IPackageManager pm) {
        this(context, pm, ActivityTaskManager.getInstance());
    UserAspectRatioManager(@NonNull Context context, @NonNull IPackageManager pm,
            @NonNull BackupManager backupManager) {
        this(context, pm, ActivityTaskManager.getInstance(), backupManager);
    }
    /**
     * Whether user aspect ratio settings is enabled for device.
@@ -192,6 +197,10 @@ public class UserAspectRatioManager {
    public void setUserMinAspectRatio(@NonNull String packageName, int uid,
            @PackageManager.UserMinAspectRatio int aspectRatio) throws RemoteException {
        mIPm.setUserMinAspectRatio(packageName, uid, aspectRatio);

        if (Flags.backupAndRestoreForUserAspectRatioSettings()) {
            mBackupManager.dataChanged();
        }
    }

    /**
+12 −0
Original line number Diff line number Diff line
@@ -16,9 +16,11 @@

package com.android.settings.backup;

import android.app.AppGlobals;
import android.app.backup.BackupAgentHelper;
import android.util.Log;

import com.android.settings.applications.appcompat.UserAspectRatioBackupHelper;
import com.android.settings.flags.Flags;
import com.android.settings.onboarding.OnboardingFeatureProvider;
import com.android.settings.overlay.FeatureFactory;
@@ -32,6 +34,7 @@ public class SettingsBackupHelper extends BackupAgentHelper {
    public static final String SOUND_BACKUP_HELPER = "SoundSettingsBackup";
    public static final String ACCESSIBILITY_APPEARANCE_BACKUP_HELPER =
            "AccessibilityAppearanceSettingsBackup";
    private static final String USER_ASPECT_RATIO_BACKUP_HELPER = "UserAspectRatioSettingsBackup";

    @Override
    public void onCreate() {
@@ -54,6 +57,15 @@ public class SettingsBackupHelper extends BackupAgentHelper {
                            this, this.getBackupRestoreEventLogger()));
            }
        }

        // Since the aconfig flag below is read-only, this class would not compile, and tests would
        // fail to find the class, even if they are testing only code beyond the flag-guarded code.
        final UserAspectRatioBackupHelper userAspectRatioBackupHelper =
                new UserAspectRatioBackupHelper(this, AppGlobals.getPackageManager(),
                        getBackupRestoreEventLogger());
        if (com.android.window.flags.Flags.backupAndRestoreForUserAspectRatioSettings()) {
            addHelper(USER_ASPECT_RATIO_BACKUP_HELPER, userAspectRatioBackupHelper);
        }
    }

    @Override
+264 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading