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

Commit 64dfe8cb authored by Mina Granic's avatar Mina Granic
Browse files

Set closest bigger aspect ratio when restored setting doesn't exist.

Some values, like split-screen, require device display aspect ratio
to understand the actual float aspect ratio.

This change uses the restore device aspect ratio for half-screen,
this will be improved if needed to use backup device dimensions.

Flag: com.android.window.flags.backup_and_restore_for_user_aspect_ratio_settings
Fixes: 407738654
Test: atest UserAspectRatioBackupManagerTest
Test: manual
Change-Id: Ia095a7254bb502af39c07996496b8174061f5e38
parent bb7b68af
Loading
Loading
Loading
Loading
+8 −2
Original line number Diff line number Diff line
@@ -634,7 +634,10 @@
    </integer-array>

    <!-- App aspect ratio settings screen, user aspect ratio override options. Must be the same
         length and order as config_userAspectRatioOverrideValues below. -->
         length and order as config_userAspectRatioOverrideValues below.

         If changed, also update the Backup & Restore logic for choosing the closest value in
         com/android/settings/applications/appcompat/UserAspectRatioBackupManager.java -->
    <string-array name="config_userAspectRatioOverrideEntries" translatable="false">
        <item>@string/user_aspect_ratio_app_default</item>
        <item>@string/user_aspect_ratio_fullscreen</item>
@@ -647,7 +650,10 @@

    <!-- App aspect ratio settings screen, user aspect ratio override options. Must be the same
         length and order as config_userAspectRatioOverrideEntries above. The values must
         correspond to PackageManager.UserMinAspectRatio -->
         correspond to PackageManager.UserMinAspectRatio.

         If changed, also update the Backup & Restore logic for choosing the closest value in
         com/android/settings/applications/appcompat/UserAspectRatioBackupManager.java -->
    <integer-array name="config_userAspectRatioOverrideValues" translatable="false">
        <item>0</item> <!-- USER_MIN_ASPECT_RATIO_UNSET -->
        <item>6</item> <!-- USER_MIN_ASPECT_RATIO_FULLSCREEN -->
+183 −15
Original line number Diff line number Diff line
@@ -16,8 +16,14 @@

package com.android.settings.applications.appcompat;

import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_16_9;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_4_3;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_APP_DEFAULT;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_SPLIT_SCREEN;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_UNSET;
import static android.hardware.display.DisplayManager.DISPLAY_CATEGORY_BUILT_IN_DISPLAYS;

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

@@ -29,10 +35,14 @@ import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.os.Handler;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Slog;
import android.view.Display;
import android.view.DisplayInfo;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.PackageMonitor;
@@ -52,12 +62,8 @@ 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 {
/** Manager class for performing Backup & Restore for per-app user aspect ratio override. */
class UserAspectRatioBackupManager {
    private static final String TAG = "UserAspRatioBackupMngr";   // must be < 23 chars
    private static final boolean DEBUG = false;

@@ -75,12 +81,15 @@ public class UserAspectRatioBackupManager {
    @BackupRestoreEventLogger.BackupRestoreError
    private static final String ERROR_ASPECT_RATIO_FAILED = "aspect_ratio_failed";

    @NonNull
    private final Context mContext;
    @NonNull
    private final IPackageManager mIPackageManager;
    @NonNull
    private final PackageManager mPackageManager;
    @VisibleForTesting
    @NonNull
    private final Set<Integer> mAvailableUserMinAspectRatioSet;
    private Set<Integer> mAvailableUserMinAspectRatioSet;
    @NonNull
    private final UserAspectRatioRestoreStorage mStorage;

@@ -89,6 +98,7 @@ public class UserAspectRatioBackupManager {
    @NonNull
    private final BackupRestoreEventLogger mLogger;

    private final Map<Integer, Float> mUserAspectRatiosPerSettingMap = new HashMap<>();

    /**
     * Helper to monitor package states for the purpose of restoring user aspect ratios.
@@ -118,6 +128,7 @@ public class UserAspectRatioBackupManager {
            @NonNull IPackageManager iPackageManager, @NonNull PackageManager packageManager,
            @NonNull BackupRestoreEventLogger logger, @NonNull Handler handler,
            @NonNull InstantSource instantSource) {
        mContext = context;
        mIPackageManager = iPackageManager;
        mPackageManager = packageManager;
        mUserId = mPackageManager.getUserId();
@@ -126,13 +137,67 @@ public class UserAspectRatioBackupManager {

        mPackageMonitor.register(context, UserHandle.of(UserHandle.USER_ALL), handler);

        final int[] userAspectRatioResourceValues = context.getResources().getIntArray(
                R.array.config_userAspectRatioOverrideValues);

        populateAvailableUserAspectRatioSettingOptions(mContext.getResources().getIntArray(
                R.array.config_userAspectRatioOverrideValues));
    }

    @VisibleForTesting
    void populateAvailableUserAspectRatioSettingOptions(
            @NonNull int[] userAspectRatioResourceValues) {
        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);

        final Rect displaySize = getSizeOfLargestDisplay();
        if (displaySize == null || displaySize.isEmpty()) {
            return;
        }
        // Always >= 1.
        final float currentLargestInternalDisplayAspectRatio = getAspectRatioForRect(displaySize);

        mUserAspectRatiosPerSettingMap.clear();
        for (int aspectRatioEnum : userAspectRatioResourceValues) {
            float aspectRatio = getAspectRatioForSetting(aspectRatioEnum,
                    currentLargestInternalDisplayAspectRatio);
            // Values that cannot be used as target aspect ratios will return -1.
            if (aspectRatio > 0) {
                mUserAspectRatiosPerSettingMap.put(aspectRatioEnum, aspectRatio);
            }
        }
    }

    /**
     * Returns the real aspect ratio that the given setting represents.
     *
     * <p>Possible values:
     * <ul>
     *    <li> Concrete user aspect ratios (e.g. 16/9): value >= 1.
     *    <li> Split screen aspect ratio: displayAspectRatio / 2, also >= 1.
     *    <li> Fullscreen: 0.5
     *    <li> Settings that will not be chosen: -1.
     * </ul>
     */
    private float getAspectRatioForSetting(int userAspectRatioSetting, float displayAspectRatio) {
        return switch (userAspectRatioSetting) {
            case USER_MIN_ASPECT_RATIO_3_2 ->  3 / 2f;
            case USER_MIN_ASPECT_RATIO_4_3 -> 4 / 3f;
            case USER_MIN_ASPECT_RATIO_16_9 -> 16 / 9f;
            case USER_MIN_ASPECT_RATIO_SPLIT_SCREEN -> makeFloatHigherOrEqualTo1(
                    displayAspectRatio / 2);
            // Fullscreen should always result in the largest area, this means the smallest aspect
            // ratio. Special value of 0.5f is used, to allow user aspect ratio setting of 1 in the
            // future.
            case USER_MIN_ASPECT_RATIO_FULLSCREEN -> 0.5f;
            // Other values, like APP_DEFAULT, will not be chosen as "closest larger".
            default -> -1;
        };
    }

    private static float makeFloatHigherOrEqualTo1(float number) {
        return number < 1 ? 1f / number : number;
    }

    /**
@@ -271,13 +336,14 @@ public class UserAspectRatioBackupManager {
                        + 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);

            final int mostSuitableAspectRatio = getSameOrClosestBiggerAspectRatio(aspectRatio);
            if (mostSuitableAspectRatio != USER_MIN_ASPECT_RATIO_UNSET) {
                mIPackageManager.setUserMinAspectRatio(pkgName, mUserId, mostSuitableAspectRatio);
                if (DEBUG) {
                    Slog.d(TAG, "Restored user aspect ratio=" + aspectRatio + " for package="
                            + pkgName);
                    Slog.d(TAG, "Restored user aspect ratio=" + mostSuitableAspectRatio
                            + " for package=" + pkgName + " . Backed-up aspect ratio was "
                            + aspectRatio);
                }
            }
        } catch (RemoteException | IllegalArgumentException e) {
@@ -286,4 +352,106 @@ public class UserAspectRatioBackupManager {
            Slog.e(TAG, "Could not restore user aspect ratio for package " + pkgName, e);
        }
    }

    private float getAspectRatioForRect(@NonNull Rect rect) {
        return makeFloatHigherOrEqualTo1((float) rect.width() / rect.height());
    }

    /**
     * Returns the most fitting aspect ratio available on this device.
     *
     * <p> If the exact aspect ratio is not available, choose the next closest one that produces a
     * bigger app surface. The goal is to produce similar UX, but also to favor bigger app area.
     * Users have already changed the user aspect ratio on their previous device, so they should be
     * able to change it again if the new aspect ratio is not ideal.
     *
     * <p>Aspect ratio is chosen in this order of priority, if available:
     * <ol>
     *     <li>Same aspect ratio setting,
     *     <li>The closest bigger available setting,
     *     <li>{@link PackageManager#USER_MIN_ASPECT_RATIO_FULLSCREEN},
     *     <li>{@link PackageManager#USER_MIN_ASPECT_RATIO_UNSET}.
     * </ol>
     */
    private int getSameOrClosestBiggerAspectRatio(int aspectRatioSetting) {
        if (mAvailableUserMinAspectRatioSet.contains(aspectRatioSetting)) {
            return aspectRatioSetting;
        }

        // TODO(b/413007174): this assumes that the size of the backed up display is similar to this
        //  device. Backup device dimensions too to calculate the exact aspect ratio that the user
        //  has set.
        final Rect displaySize = getSizeOfLargestDisplay();
        if (displaySize == null || displaySize.isEmpty()) {
            return USER_MIN_ASPECT_RATIO_UNSET;
        }
        float realRestoredAspectRatio = getAspectRatioForSetting(aspectRatioSetting,
                getAspectRatioForRect(displaySize));

        // Bigger aspect ratio means smaller area, so the goal is to find the closest aspect ratio
        // float value that is less than the restored aspect ratio. Fullscreen has the aspect ratio
        // equal to 0.5 (smallest value), so the result will gravitate towards fullscreen.
        int bestSetting = 0;
        float closestSmallerAspectRatioValue = 0;
        for (Integer setting : mUserAspectRatiosPerSettingMap.keySet()) {
            float aspectRatio = mUserAspectRatiosPerSettingMap.get(setting);
            // Bigger area (goal) results in a smaller aspect ratio (closer to 1), so bigger value
            // does not work. Fullscreen has a special value of 0.5f, to allow min aspect ratio
            // setting of 1 in the future.
            // Values < 0 are not applicable for this comparison.
            if (aspectRatio > realRestoredAspectRatio || aspectRatio < 0) {
                continue;
            }
            // If current value is smaller than the restored, make sure it is the closest so far.
            if (aspectRatio > closestSmallerAspectRatioValue) {
                closestSmallerAspectRatioValue = aspectRatio;
                bestSetting = setting;
            }
        }
        if (bestSetting == 0) {
            // This device is smaller than the real aspect ratio of the new device. Choose
            // fullscreen if possible.
            if (mAvailableUserMinAspectRatioSet.contains(USER_MIN_ASPECT_RATIO_FULLSCREEN)) {
                return USER_MIN_ASPECT_RATIO_FULLSCREEN;
            } else {
                Slog.w(TAG, "Unable to find a suitable aspect ratio for restored: "
                        + aspectRatioSetting);
                return USER_MIN_ASPECT_RATIO_UNSET;
            }
        }

        return bestSetting;
    }

    // Visible for testing because DisplayManager cannot be mocked, so using the real implementation
    // in the test and making sure it is well tested.

    /**
     * Returns the dimensions of the largest built-in display, even if inactive or disabled.
     *
     * <p>This method assumes that the largest display will be the one to measure aspect ratio by,
     * as it is unable to check whether {@code ignoreOrientationRequest == true}.
     */
    @VisibleForTesting
    @Nullable
    Rect getSizeOfLargestDisplay() {
        final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
        if (displayManager == null) {
            return null;
        }
        final Display[] displays = displayManager.getDisplays(
                /* flags= */ DISPLAY_CATEGORY_BUILT_IN_DISPLAYS);
        final Rect maxDimensions = new Rect();
        for (Display display: displays) {
            final DisplayInfo outDisplayInfo = new DisplayInfo();
            display.getDisplayInfo(outDisplayInfo);
            final int width = outDisplayInfo.getNaturalWidth();
            final int height = outDisplayInfo.getNaturalHeight();
            if (width * height > maxDimensions.width() * maxDimensions.height())  {
                maxDimensions.set(0 , 0, width, height);
            }
        }
        return maxDimensions;
    }

}
+77 −0
Original line number Diff line number Diff line
@@ -16,6 +16,9 @@

package com.android.settings.applications.appcompat;

import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_16_9;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_4_3;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_SPLIT_SCREEN;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_UNSET;
@@ -108,6 +111,10 @@ public class UserAspectRatioBackupManagerTest {
        mBackupManager = new UserAspectRatioBackupManager(mContext, mMockIPackageManager,
                mMockPackageManager, mLogger, broadcastHandlerThread.getThreadHandler(),
                new FakeInstantSource());
        mBackupManager.populateAvailableUserAspectRatioSettingOptions(new int[] {
                USER_MIN_ASPECT_RATIO_FULLSCREEN,
                USER_MIN_ASPECT_RATIO_SPLIT_SCREEN,
                USER_MIN_ASPECT_RATIO_4_3});
    }

    @Test
@@ -230,6 +237,76 @@ public class UserAspectRatioBackupManagerTest {
                DEFAULT_USER_ID, USER_MIN_ASPECT_RATIO_FULLSCREEN);
    }

    @Test
    public void testRestoredNotAvailable_3_2_to_4_3() throws Exception {
        final byte[] out = writeTestPayload(Map.of(DEFAULT_PACKAGE_NAME,
                USER_MIN_ASPECT_RATIO_3_2));
        setUpInstalledPackages(List.of(DEFAULT_PACKAGE_NAME));
        mBackupManager.populateAvailableUserAspectRatioSettingOptions(new int[] {
                USER_MIN_ASPECT_RATIO_4_3,
                USER_MIN_ASPECT_RATIO_16_9,
                USER_MIN_ASPECT_RATIO_FULLSCREEN
        });
        mBackupManager.stageAndApplyRestoredPayload(out);

        // User aspect ratio 4/3 is restored, as that is the first bigger one (1.5 -> 1.33).
        verify(mMockIPackageManager).setUserMinAspectRatio(DEFAULT_PACKAGE_NAME,
                DEFAULT_USER_ID, USER_MIN_ASPECT_RATIO_4_3);
    }

    @Test
    public void testRestoredNotAvailable_16_9_to_3_2() throws Exception {
        final byte[] out = writeTestPayload(Map.of(DEFAULT_PACKAGE_NAME,
                USER_MIN_ASPECT_RATIO_16_9));
        setUpInstalledPackages(List.of(DEFAULT_PACKAGE_NAME));
        mBackupManager.populateAvailableUserAspectRatioSettingOptions(new int[] {
                USER_MIN_ASPECT_RATIO_3_2,
                USER_MIN_ASPECT_RATIO_4_3,
                USER_MIN_ASPECT_RATIO_FULLSCREEN
        });
        mBackupManager.stageAndApplyRestoredPayload(out);

        // User aspect ratio 3/2 is restored, as that is the first bigger one (1.78 -> 1.5).
        verify(mMockIPackageManager).setUserMinAspectRatio(DEFAULT_PACKAGE_NAME,
                DEFAULT_USER_ID, USER_MIN_ASPECT_RATIO_3_2);
    }

    @Test
    public void testRestoredNotAvailable_4_3_to_fullscreen() throws Exception {
        final byte[] out = writeTestPayload(Map.of(DEFAULT_PACKAGE_NAME,
                USER_MIN_ASPECT_RATIO_4_3));
        setUpInstalledPackages(List.of(DEFAULT_PACKAGE_NAME));
        mBackupManager.populateAvailableUserAspectRatioSettingOptions(new int[] {
                USER_MIN_ASPECT_RATIO_3_2,
                USER_MIN_ASPECT_RATIO_16_9,
                USER_MIN_ASPECT_RATIO_FULLSCREEN
        });
        mBackupManager.stageAndApplyRestoredPayload(out);

        // Fullscreen is restored, as that is the first bigger one (1.33 -> 1.0).
        verify(mMockIPackageManager).setUserMinAspectRatio(DEFAULT_PACKAGE_NAME,
                DEFAULT_USER_ID, USER_MIN_ASPECT_RATIO_FULLSCREEN);
    }

    @Test
    public void testRestored_splitScreenToSplitScreenWhenDimensionsAreTheSame() throws Exception {
        final byte[] out = writeTestPayload(Map.of(DEFAULT_PACKAGE_NAME,
                USER_MIN_ASPECT_RATIO_SPLIT_SCREEN));
        setUpInstalledPackages(List.of(DEFAULT_PACKAGE_NAME));
        mBackupManager.populateAvailableUserAspectRatioSettingOptions(new int[] {
                USER_MIN_ASPECT_RATIO_3_2,
                USER_MIN_ASPECT_RATIO_4_3,
                USER_MIN_ASPECT_RATIO_16_9,
                USER_MIN_ASPECT_RATIO_SPLIT_SCREEN,
                USER_MIN_ASPECT_RATIO_FULLSCREEN
        });
        mBackupManager.stageAndApplyRestoredPayload(out);

        // Half screen maps to itself when the device dimensions are the same.
        verify(mMockIPackageManager).setUserMinAspectRatio(DEFAULT_PACKAGE_NAME,
                DEFAULT_USER_ID, USER_MIN_ASPECT_RATIO_SPLIT_SCREEN);
    }

    /**
     * Verifies that nothing was restored for any package.
     *