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

Commit 6675244b authored by Mina Granic's avatar Mina Granic Committed by Android (Google) Code Review
Browse files

Merge "Set restored aspect ratio for newly installed apps in a service." into main

parents ddef9a0d d3f7489c
Loading
Loading
Loading
Loading
+47 −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.appwindowlayout;

import android.annotation.NonNull;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.PackageMonitor;

/**
 * {@link PackageMonitor} for {@link AppWindowLayoutSettingsService}.
 *
 * @hide
 */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public class AppWindowLayoutSettingsPackageMonitor extends PackageMonitor {
    private PackageAddedCallback mPackageAddedCallback;

    void setCallback(@NonNull PackageAddedCallback packageAddedCallback) {
        mPackageAddedCallback = packageAddedCallback;
    }

    @Override
    public void onPackageAdded(String packageName, int uid) {
        super.onPackageAdded(packageName, uid);
        final int userId = getChangingUserId();
        mPackageAddedCallback.onPackageAdded(packageName, userId);
    }

    interface PackageAddedCallback {
        void onPackageAdded(String packageName, int userId);
    }
}
+134 −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.appwindowlayout;

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

import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Environment;

import com.android.internal.annotations.VisibleForTesting;

import java.io.File;
import java.time.Duration;
import java.time.Instant;
import java.time.InstantSource;

/**
 * Storage for restored app window layout settings, for apps that are not installed yet.
 *
 * <p>This class uses SharedPreferences to preserve data. Only non-committed data is held in the
 * storage. Any stored data waiting for apps to be installed will be deleted after 7 days.
 *
 * @hide
 */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public class AppWindowLayoutSettingsRestoreStorage {
    // TODO(b/414361710): Consider joining this class implementation with the Settings app
    //  UserAspectRatioRestoreStorage, or removing the Settings class implementation if it is not
    //  needed after restoreUserAspectRatioSettingsUsingService flag is launched.
    @VisibleForTesting
    static final String ASPECT_RATIO_STAGED_DATA_PREFS = "AspectRatioStagedDataPrefs.xml";
    // Storage for commit time, to be able to clean up after expiry date.
    @VisibleForTesting
    static final String RESTORE_TIME_STAGED_DATA_PREFS = "RestoreTimeStagedDataPrefs.xml";
    @VisibleForTesting
    static final String KEY_STAGED_DATA_TIME = "staged_data_time";
    // 7 days after completing restore, the staged data will be deleted automatically. This will
    // erase restored user aspect ratios for apps that haven't been installed since.
    private static final Duration EXPIRY_DURATION = Duration.ofDays(7);

    @NonNull
    private final Context mContext;
    @UserIdInt
    private final int mUserId;
    @NonNull
    private final InstantSource mInstantSource;

    // SharedPreferences to store restored user aspect ratios for not-yet-installed packages.
    @NonNull
    private final SharedPreferences mUserAspectRatioSharedPreferences;
    // SharedPreferences to store restore time, for the purposes of cleanup.
    @NonNull
    private final SharedPreferences mRestoreTimeSharedPreferences;

    AppWindowLayoutSettingsRestoreStorage(@NonNull Context context, @UserIdInt int userId,
            @NonNull InstantSource instantSource) {
        mContext = context;
        mUserId = userId;
        mInstantSource = instantSource;

        mUserAspectRatioSharedPreferences = createSharedPrefs(mContext.getPackageName()
                + "." + ASPECT_RATIO_STAGED_DATA_PREFS);
        mRestoreTimeSharedPreferences = createSharedPrefs(mContext.getPackageName()
                + "." + RESTORE_TIME_STAGED_DATA_PREFS);
    }

    void setStagedRestoreTime() {
        // Store restore time, for the purpose of cleanup after expiry date.
        mRestoreTimeSharedPreferences.edit().putLong(KEY_STAGED_DATA_TIME,
                mInstantSource.millis()).commit();
    }

    /**
     * Stores package name and user aspect ratio that is restored, but cannot be committed at the
     * moment, for example because given package has not been installed yet.
     */
    void storePackageAndUserAspectRatio(@NonNull String packageName,
            @PackageManager.UserMinAspectRatio int userAspectRatio) {
        mUserAspectRatioSharedPreferences.edit().putInt(packageName, userAspectRatio).commit();
        setStagedRestoreTime();
    }

    int getAndRemoveUserAspectRatioForPackage(@NonNull String packageName) {
        removeExpiredDataIfNeeded();
        final int aspectRatio = mUserAspectRatioSharedPreferences.getInt(packageName,
                USER_MIN_ASPECT_RATIO_UNSET);
        mUserAspectRatioSharedPreferences.edit().remove(packageName).commit();
        return aspectRatio;
    }

    @NonNull
    private SharedPreferences createSharedPrefs(@NonNull String fileName) {
        final File prefsFile = new File(Environment.getDataSystemDeDirectory(mUserId), fileName);
        return mContext.createDeviceProtectedStorageContext().getSharedPreferences(prefsFile,
                Context.MODE_PRIVATE);
    }

    private void removeExpiredDataIfNeeded() {
        if (!mRestoreTimeSharedPreferences.contains(KEY_STAGED_DATA_TIME)) {
            // Restore not yet completed (too early to clean up), or already cleaned up.
            return;
        }

        final Instant restoreTime = Instant.ofEpochMilli(mRestoreTimeSharedPreferences.getLong(
                KEY_STAGED_DATA_TIME, 0));
        if (Duration.between(restoreTime, mInstantSource.instant()).compareTo(EXPIRY_DURATION)
                >= 0) {
            // Remove the restore time and all data to restore.
            mUserAspectRatioSharedPreferences.edit().clear().commit();
            mRestoreTimeSharedPreferences.edit().clear().commit();
        }
    }

    boolean hasDataStored() {
        return !mUserAspectRatioSharedPreferences.getAll().isEmpty();
    }
}
+215 −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.appwindowlayout;

import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_UNSET;
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;

import android.annotation.UserIdInt;
import android.app.AppGlobals;
import android.content.Context;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.os.HandlerThread;
import android.os.UserHandle;
import android.util.Slog;
import android.util.SparseArray;

import androidx.annotation.NonNull;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.PackageMonitor;
import com.android.server.LocalServices;
import com.android.server.SystemService;

import java.time.Clock;
import java.util.List;

/**
 * {@link SystemService} that restores per-app window layout settings when apps are installed.
 *
 * <p>This service is used by the Settings app to persist restored user aspect ratio settings, for
 * apps that are not installed at the time of Setup Wizard and Restore (which is most
 * non-prepackaged apps). The Settings app process can be killed at any point, so it is not suitable
 * for listening to package changes.
 *
 * <p>{@link AppWindowLayoutSettingsService} registers a {@link PackageMonitor} to listen to
 * package-added signals, and when an app is restored, calls
 * {@link IPackageManager#setUserMinAspectRatio(String, int, int)}.
 *
 * @hide
 */
public class AppWindowLayoutSettingsService extends SystemService {
    private static final String TAG = "AppWinLayoutSetService";

    private static final boolean DEBUG = false;

    @NonNull
    private final Context mContext;
    @NonNull
    private final IPackageManager mIPackageManager;

    private final Object mLock = new Object();

    @NonNull
    @GuardedBy("mLock")
    private final SparseArray<AppWindowLayoutSettingsRestoreStorage> mUserStorageMap =
            new SparseArray<>();

    private boolean mIsPackageMonitorRegistered = false;

    @NonNull
    @GuardedBy("mLock")
    private final AppWindowLayoutSettingsPackageMonitor mPackageMonitor;

    private final HandlerThread mBackgroundThread;

    public AppWindowLayoutSettingsService(@NonNull Context context,
            @NonNull List<Class<?>> dependencies) {
        super(context, dependencies);
        mContext = context;
        mIPackageManager = AppGlobals.getPackageManager();
        mPackageMonitor = new AppWindowLayoutSettingsPackageMonitor();
        mPackageMonitor.setCallback(this::onPackageAdded);
        final HandlerThread handlerThread = new HandlerThread("AppWinLayoutSetService",
                THREAD_PRIORITY_BACKGROUND);
        mBackgroundThread = handlerThread;
        mBackgroundThread.start();
    }

    public AppWindowLayoutSettingsService(@NonNull Context context) {
        super(context);
        mContext = context;
        mIPackageManager = AppGlobals.getPackageManager();
        mPackageMonitor = new AppWindowLayoutSettingsPackageMonitor();
        mPackageMonitor.setCallback(this::onPackageAdded);
        final HandlerThread handlerThread = new HandlerThread("AppWinLayoutSetService",
                THREAD_PRIORITY_BACKGROUND);
        mBackgroundThread = handlerThread;
        mBackgroundThread.start();
    }

    @VisibleForTesting
    AppWindowLayoutSettingsService(@NonNull Context context,
            @NonNull IPackageManager iPackageManager,
            @NonNull AppWindowLayoutSettingsPackageMonitor packageMonitor) {
        super(context);
        mContext = context;
        mIPackageManager = iPackageManager;
        mPackageMonitor = packageMonitor;
        mPackageMonitor.setCallback(this::onPackageAdded);
        mBackgroundThread = new HandlerThread("AppWinLayoutSetService",
                THREAD_PRIORITY_BACKGROUND);
        mBackgroundThread.start();
    }

    @Override
    public void onStart() {
        synchronized (mLock) {
            LocalServices.addService(AppWindowLayoutSettingsService.class, this);

            // Register package monitor now, because restore might have already been completed, but
            // the device rebooted before all apps were installed. PackageMonitor will be
            // unregistered on the first packageAdded signa if there is no restore data, and
            // registered again if restore data is received.
            registerPackageMonitor();
        }
    }
    /**
     * Stores packageName and aspectRatio for a given user to be set when package is installed.
     *
     * <p>This method also registers {@link AppWindowLayoutSettingsPackageMonitor} for package
     * updates.
     */
    // TODO(b/414381398): expose this API for apps to call directly, with a privileged permission.
    public void awaitPackageInstallForAspectRatio(@NonNull String packageName,
            @UserIdInt int userId,
            @PackageManager.UserMinAspectRatio int aspectRatio) {
        synchronized (mLock) {
            createAndGetStorage(userId).storePackageAndUserAspectRatio(packageName, aspectRatio);

            registerPackageMonitor();
        }
    }

    private void onPackageAdded(@NonNull String packageName, @UserIdInt int userId) {
        synchronized (mLock) {
            final AppWindowLayoutSettingsRestoreStorage storage = createAndGetStorage(userId);
            final int aspectRatio = storage.getAndRemoveUserAspectRatioForPackage(packageName);
            if (aspectRatio != USER_MIN_ASPECT_RATIO_UNSET) {
                checkExistingAspectRatioAndApplyRestore(packageName, userId, aspectRatio);
            }

            // If all restore data has been removed - either because all apps with restore data have
            // been restored or because the data has expired - stop listening to package updates.
            if (!storage.hasDataStored()) {
                unregisterPackageMonitor();
            }
        }
    }

    @GuardedBy("mLock")
    private AppWindowLayoutSettingsRestoreStorage createAndGetStorage(@UserIdInt int userId) {
        if (mUserStorageMap.get(userId) == null) {
            mUserStorageMap.put(userId, new AppWindowLayoutSettingsRestoreStorage(mContext, userId,
                    Clock.systemUTC()));
        }
        return mUserStorageMap.get(userId);
    }

    @GuardedBy("mLock")
    private void registerPackageMonitor() {
        if (!mIsPackageMonitorRegistered) {
            mIsPackageMonitorRegistered = true;
            mPackageMonitor.register(this.getContext(), mBackgroundThread.getLooper(),
                    UserHandle.ALL, true);
        }
    }

    @GuardedBy("mLock")
    private void unregisterPackageMonitor() {
        if (mIsPackageMonitorRegistered) {
            mIsPackageMonitorRegistered = false;
            mPackageMonitor.unregister();
        }
    }

    /** Applies the restore for per-app user set min aspect ratio. */
    private void checkExistingAspectRatioAndApplyRestore(@NonNull String pkgName, int userId,
            @PackageManager.UserMinAspectRatio int aspectRatio) {
        try {
            final int existingUserAspectRatio = mIPackageManager.getUserMinAspectRatio(pkgName,
                    userId);
            // Don't apply the restore if the aspect ratio have already been set for the app.
            // Packages which are not yet installed will return `USER_MIN_ASPECT_RATIO_UNSET`.
            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;
            }

            mIPackageManager.setUserMinAspectRatio(pkgName, userId, aspectRatio);
            if (DEBUG) {
                Slog.d(TAG, "Restored user aspect ratio=" + aspectRatio + " for package="
                        + pkgName);
            }
        } catch (Exception e) {
            Slog.e(TAG, "Could not restore user aspect ratio for package " + pkgName, e);
        }
    }
}
+6 −0
Original line number Diff line number Diff line
# Bug component: 970984
# Large Screen Experiences App Compat
gracielawputri@google.com
mcarli@google.com
mariiasand@google.com
minagranic@google.com
 No newline at end of file
+15 −0
Original line number Diff line number Diff line
@@ -212,6 +212,7 @@ import com.android.server.SystemConfig;
import com.android.server.ThreadPriorityBooster;
import com.android.server.Watchdog;
import com.android.server.apphibernation.AppHibernationManagerInternal;
import com.android.server.appwindowlayout.AppWindowLayoutSettingsService;
import com.android.server.art.DexUseManagerLocal;
import com.android.server.art.model.DeleteResult;
import com.android.server.compat.CompatChange;
@@ -6481,6 +6482,20 @@ public class PackageManagerService implements PackageSender, TestUtilityService
            final PackageStateInternal packageState = snapshot
                    .getPackageStateForInstalledAndFiltered(packageName, callingUid, userId);
            if (packageState == null) {
                if (com.android.window.flags.Flags.restoreUserAspectRatioSettingsUsingService()) {
                    // Pass along the request to `AppWindowLayoutSettingsService`, which will retry
                    // to set the user aspect ratio after the package has been installed.
                    final AppWindowLayoutSettingsService appWindowLayoutSettingsService =
                            LocalServices.getService(AppWindowLayoutSettingsService.class);
                    if (appWindowLayoutSettingsService == null) {
                        Slog.w(TAG, "Could not find AppWindowLayoutSettingsService.");
                        return;
                    }
                    // TODO(b/414381398): expose this API to the Settings app to call directly, so
                    //  that `setUserMinAspectRatio()` becomes a no-op when app is not installed.
                    appWindowLayoutSettingsService.awaitPackageInstallForAspectRatio(packageName,
                            userId, aspectRatio);
                }
                return;
            }

Loading