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

Commit 3e99c16a authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Backup and Restore User Aspect Ratio Settings." into main

parents aa581604 fdb9ac7d
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