/* * Copyright (C) 2016 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.wallpaperbackup; import static android.app.WallpaperManager.FLAG_LOCK; import static android.app.WallpaperManager.FLAG_SYSTEM; import static android.app.WallpaperManager.ORIENTATION_UNKNOWN; import static android.os.ParcelFileDescriptor.MODE_READ_ONLY; import static com.android.wallpaperbackup.WallpaperEventLogger.ERROR_INELIGIBLE; import static com.android.wallpaperbackup.WallpaperEventLogger.ERROR_NO_METADATA; import static com.android.wallpaperbackup.WallpaperEventLogger.ERROR_NO_WALLPAPER; import static com.android.wallpaperbackup.WallpaperEventLogger.ERROR_QUOTA_EXCEEDED; import static com.android.window.flags.Flags.multiCrop; import android.app.AppGlobals; import android.app.WallpaperManager; import android.app.backup.BackupAgent; import android.app.backup.BackupDataInput; import android.app.backup.BackupDataOutput; import android.app.backup.BackupManager; import android.app.backup.BackupRestoreEventLogger.BackupRestoreError; import android.app.backup.FullBackupDataOutput; import android.content.ComponentName; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.IPackageManager; import android.content.pm.PackageInfo; import android.graphics.BitmapFactory; import android.graphics.Point; import android.graphics.Rect; import android.hardware.display.DisplayManager; import android.os.FileUtils; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.UserHandle; import android.provider.Settings; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; import android.util.Xml; import android.view.Display; import android.view.DisplayInfo; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.PackageMonitor; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * Backs up and restores wallpaper and metadata related to it. * * This agent has its own package because it does full backup as opposed to SystemBackupAgent * which does key/value backup. * * This class stages wallpaper files for backup by copying them into its own directory because of * the following reasons: * * * * There are 3 files to back up: * * * On restore, the metadata file is parsed and {@link WallpaperManager} APIs are used to set the * wallpaper. Note that if there's a live wallpaper, the live wallpaper package name will be * part of the metadata file and the wallpaper will be applied when the package it's installed. */ public class WallpaperBackupAgent extends BackupAgent { private static final String TAG = "WallpaperBackup"; private static final boolean DEBUG = false; // Names of our local-data stage files @VisibleForTesting static final String SYSTEM_WALLPAPER_STAGE = "wallpaper-stage"; @VisibleForTesting static final String LOCK_WALLPAPER_STAGE = "wallpaper-lock-stage"; @VisibleForTesting static final String WALLPAPER_INFO_STAGE = "wallpaper-info-stage"; @VisibleForTesting static final String WALLPAPER_BACKUP_DEVICE_INFO_STAGE = "wallpaper-backup-device-info-stage"; static final String EMPTY_SENTINEL = "empty"; static final String QUOTA_SENTINEL = "quota"; // Shared preferences constants. static final String PREFS_NAME = "wbprefs.xml"; static final String SYSTEM_GENERATION = "system_gen"; static final String LOCK_GENERATION = "lock_gen"; static final float DEFAULT_ACCEPTABLE_PARALLAX = 0.2f; // If this file exists, it means we exceeded our quota last time private File mQuotaFile; private boolean mQuotaExceeded; private WallpaperManager mWallpaperManager; private WallpaperEventLogger mEventLogger; private BackupManager mBackupManager; private boolean mSystemHasLiveComponent; private boolean mLockHasLiveComponent; private DisplayManager mDisplayManager; @Override public void onCreate() { if (DEBUG) { Slog.v(TAG, "onCreate()"); } mWallpaperManager = getSystemService(WallpaperManager.class); mQuotaFile = new File(getFilesDir(), QUOTA_SENTINEL); mQuotaExceeded = mQuotaFile.exists(); if (DEBUG) { Slog.v(TAG, "quota file " + mQuotaFile.getPath() + " exists=" + mQuotaExceeded); } mBackupManager = new BackupManager(getBaseContext()); mEventLogger = new WallpaperEventLogger(mBackupManager, /* wallpaperAgent */ this); mDisplayManager = getSystemService(DisplayManager.class); } @Override public void onFullBackup(FullBackupDataOutput data) throws IOException { try { // We always back up this 'empty' file to ensure that the absence of // storable wallpaper imagery still produces a non-empty backup data // stream, otherwise it'd simply be ignored in preflight. final File empty = new File(getFilesDir(), EMPTY_SENTINEL); if (!empty.exists()) { FileOutputStream touch = new FileOutputStream(empty); touch.close(); } backupFile(empty, data); SharedPreferences sharedPrefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); // Check the IDs of the wallpapers that we backed up last time. If they haven't // changed, we won't re-stage them for backup and use the old staged versions to avoid // disk churn. final int lastSysGeneration = sharedPrefs.getInt(SYSTEM_GENERATION, /* defValue= */ -1); final int lastLockGeneration = sharedPrefs.getInt(LOCK_GENERATION, /* defValue= */ -1); final int sysGeneration = mWallpaperManager.getWallpaperId(FLAG_SYSTEM); final int lockGeneration = mWallpaperManager.getWallpaperId(FLAG_LOCK); final boolean sysChanged = (sysGeneration != lastSysGeneration); final boolean lockChanged = (lockGeneration != lastLockGeneration); if (DEBUG) { Slog.v(TAG, "sysGen=" + sysGeneration + " : sysChanged=" + sysChanged); Slog.v(TAG, "lockGen=" + lockGeneration + " : lockChanged=" + lockChanged); } // Due to the way image vs live wallpaper backup logic is intermingled, for logging // purposes first check if we have live components for each wallpaper to avoid // over-reporting errors. mSystemHasLiveComponent = mWallpaperManager.getWallpaperInfo(FLAG_SYSTEM) != null; mLockHasLiveComponent = mWallpaperManager.getWallpaperInfo(FLAG_LOCK) != null; // performing backup of each file based on order of importance backupWallpaperInfoFile(/* sysOrLockChanged= */ sysChanged || lockChanged, data); backupSystemWallpaperFile(sharedPrefs, sysChanged, sysGeneration, data); backupLockWallpaperFileIfItExists(sharedPrefs, lockChanged, lockGeneration, data); backupDeviceInfoFile(data); } catch (Exception e) { Slog.e(TAG, "Unable to back up wallpaper", e); mEventLogger.onBackupException(e); } finally { // Even if this time we had to back off on attempting to store the lock image // due to exceeding the data quota, try again next time. This will alternate // between "try both" and "only store the primary image" until either there // is no lock image to store, or the quota is raised, or both fit under the // quota. mQuotaFile.delete(); } } /** * This method backs up the device dimension information. The device data will always get * overwritten when triggering a backup */ private void backupDeviceInfoFile(FullBackupDataOutput data) throws IOException { final File deviceInfoStage = new File(getFilesDir(), WALLPAPER_BACKUP_DEVICE_INFO_STAGE); // save the dimensions of the device with xml formatting Point dimensions = getScreenDimensions(); Display smallerDisplay = getSmallerDisplayIfExists(); Point secondaryDimensions = smallerDisplay != null ? getRealSize(smallerDisplay) : new Point(0, 0); deviceInfoStage.createNewFile(); FileOutputStream fstream = new FileOutputStream(deviceInfoStage, false); TypedXmlSerializer out = Xml.resolveSerializer(fstream); out.startDocument(null, true); out.startTag(null, "dimensions"); out.startTag(null, "width"); out.text(String.valueOf(dimensions.x)); out.endTag(null, "width"); out.startTag(null, "height"); out.text(String.valueOf(dimensions.y)); out.endTag(null, "height"); if (smallerDisplay != null) { out.startTag(null, "secondarywidth"); out.text(String.valueOf(secondaryDimensions.x)); out.endTag(null, "secondarywidth"); out.startTag(null, "secondaryheight"); out.text(String.valueOf(secondaryDimensions.y)); out.endTag(null, "secondaryheight"); } out.endTag(null, "dimensions"); out.endDocument(); fstream.flush(); FileUtils.sync(fstream); fstream.close(); if (DEBUG) Slog.v(TAG, "Storing device dimension data"); backupFile(deviceInfoStage, data); } private void backupWallpaperInfoFile(boolean sysOrLockChanged, FullBackupDataOutput data) throws IOException { final ParcelFileDescriptor wallpaperInfoFd = mWallpaperManager.getWallpaperInfoFile(); if (wallpaperInfoFd == null) { Slog.w(TAG, "Wallpaper metadata file doesn't exist"); // If we have live components, getting the file to back up somehow failed, so log it // as an error. if (mSystemHasLiveComponent) { mEventLogger.onSystemLiveWallpaperBackupFailed(ERROR_NO_METADATA); } if (mLockHasLiveComponent) { mEventLogger.onLockLiveWallpaperBackupFailed(ERROR_NO_METADATA); } return; } final File infoStage = new File(getFilesDir(), WALLPAPER_INFO_STAGE); if (sysOrLockChanged || !infoStage.exists()) { if (DEBUG) Slog.v(TAG, "New wallpaper configuration; copying"); copyFromPfdToFileAndClosePfd(wallpaperInfoFd, infoStage); } if (DEBUG) Slog.v(TAG, "Storing wallpaper metadata"); backupFile(infoStage, data); // We've backed up the info file which contains the live component, so log it as success if (mSystemHasLiveComponent) { mEventLogger.onSystemLiveWallpaperBackedUp( mWallpaperManager.getWallpaperInfo(FLAG_SYSTEM)); } if (mLockHasLiveComponent) { mEventLogger.onLockLiveWallpaperBackedUp(mWallpaperManager.getWallpaperInfo(FLAG_LOCK)); } } private void backupSystemWallpaperFile(SharedPreferences sharedPrefs, boolean sysChanged, int sysGeneration, FullBackupDataOutput data) throws IOException { if (!mWallpaperManager.isWallpaperBackupEligible(FLAG_SYSTEM)) { Slog.d(TAG, "System wallpaper ineligible for backup"); logSystemImageErrorIfNoLiveComponent(ERROR_INELIGIBLE); return; } final ParcelFileDescriptor systemWallpaperImageFd = mWallpaperManager.getWallpaperFile( FLAG_SYSTEM, /* getCropped= */ false); if (systemWallpaperImageFd == null) { Slog.w(TAG, "System wallpaper doesn't exist"); logSystemImageErrorIfNoLiveComponent(ERROR_NO_WALLPAPER); return; } final File imageStage = new File(getFilesDir(), SYSTEM_WALLPAPER_STAGE); if (sysChanged || !imageStage.exists()) { if (DEBUG) Slog.v(TAG, "New system wallpaper; copying"); copyFromPfdToFileAndClosePfd(systemWallpaperImageFd, imageStage); } if (DEBUG) Slog.v(TAG, "Storing system wallpaper image"); backupFile(imageStage, data); sharedPrefs.edit().putInt(SYSTEM_GENERATION, sysGeneration).apply(); mEventLogger.onSystemImageWallpaperBackedUp(); } private void logSystemImageErrorIfNoLiveComponent(@BackupRestoreError String error) { if (mSystemHasLiveComponent) { return; } mEventLogger.onSystemImageWallpaperBackupFailed(error); } private void backupLockWallpaperFileIfItExists(SharedPreferences sharedPrefs, boolean lockChanged, int lockGeneration, FullBackupDataOutput data) throws IOException { final File lockImageStage = new File(getFilesDir(), LOCK_WALLPAPER_STAGE); // This means there's no lock wallpaper set by the user. if (lockGeneration == -1) { if (lockChanged && lockImageStage.exists()) { if (DEBUG) Slog.v(TAG, "Removed lock wallpaper; deleting"); lockImageStage.delete(); } Slog.d(TAG, "No lockscreen wallpaper set, add nothing to backup"); sharedPrefs.edit().putInt(LOCK_GENERATION, lockGeneration).apply(); logLockImageErrorIfNoLiveComponent(ERROR_NO_WALLPAPER); return; } if (!mWallpaperManager.isWallpaperBackupEligible(FLAG_LOCK)) { Slog.d(TAG, "Lock screen wallpaper ineligible for backup"); logLockImageErrorIfNoLiveComponent(ERROR_INELIGIBLE); return; } final ParcelFileDescriptor lockWallpaperFd = mWallpaperManager.getWallpaperFile( FLAG_LOCK, /* getCropped= */ false); // If we get to this point, that means lockGeneration != -1 so there's a lock wallpaper // set, but we can't find it. if (lockWallpaperFd == null) { Slog.w(TAG, "Lock wallpaper doesn't exist"); logLockImageErrorIfNoLiveComponent(ERROR_NO_WALLPAPER); return; } if (mQuotaExceeded) { Slog.w(TAG, "Not backing up lock screen wallpaper. Quota was exceeded last time"); logLockImageErrorIfNoLiveComponent(ERROR_QUOTA_EXCEEDED); return; } if (lockChanged || !lockImageStage.exists()) { if (DEBUG) Slog.v(TAG, "New lock wallpaper; copying"); copyFromPfdToFileAndClosePfd(lockWallpaperFd, lockImageStage); } if (DEBUG) Slog.v(TAG, "Storing lock wallpaper image"); backupFile(lockImageStage, data); sharedPrefs.edit().putInt(LOCK_GENERATION, lockGeneration).apply(); mEventLogger.onLockImageWallpaperBackedUp(); } private void logLockImageErrorIfNoLiveComponent(@BackupRestoreError String error) { if (mLockHasLiveComponent) { return; } mEventLogger.onLockImageWallpaperBackupFailed(error); } /** * Copies the contents of the given {@code pfd} to the given {@code file}. * * All resources used in the process including the {@code pfd} will be closed. */ private static void copyFromPfdToFileAndClosePfd(ParcelFileDescriptor pfd, File file) throws IOException { try (ParcelFileDescriptor.AutoCloseInputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(pfd); FileOutputStream outputStream = new FileOutputStream(file) ) { FileUtils.copy(inputStream, outputStream); } } private static String readText(TypedXmlPullParser parser) throws IOException, XmlPullParserException { String result = ""; if (parser.next() == XmlPullParser.TEXT) { result = parser.getText(); parser.nextTag(); } return result; } @VisibleForTesting // fullBackupFile is final, so we intercept backups here in tests. protected void backupFile(File file, FullBackupDataOutput data) { fullBackupFile(file, data); } @Override public void onQuotaExceeded(long backupDataBytes, long quotaBytes) { Slog.i(TAG, "Quota exceeded (" + backupDataBytes + " vs " + quotaBytes + ')'); try (FileOutputStream f = new FileOutputStream(mQuotaFile)) { f.write(0); } catch (Exception e) { Slog.w(TAG, "Unable to record quota-exceeded: " + e.getMessage()); } } // We use the default onRestoreFile() implementation that will recreate our stage files, // then post-process in onRestoreFinished() to apply the new wallpaper. @Override public void onRestoreFinished() { Slog.v(TAG, "onRestoreFinished()"); final File filesDir = getFilesDir(); final File infoStage = new File(filesDir, WALLPAPER_INFO_STAGE); final File imageStage = new File(filesDir, SYSTEM_WALLPAPER_STAGE); final File lockImageStage = new File(filesDir, LOCK_WALLPAPER_STAGE); final File deviceDimensionsStage = new File(filesDir, WALLPAPER_BACKUP_DEVICE_INFO_STAGE); boolean lockImageStageExists = lockImageStage.exists(); try { // Parse the device dimensions of the source device Pair sourceDeviceDimensions = parseDeviceDimensions( deviceDimensionsStage); // First parse the live component name so that we know for logging if we care about // logging errors with the image restore. ComponentName wpService = parseWallpaperComponent(infoStage, "wp"); mSystemHasLiveComponent = wpService != null; ComponentName kwpService = parseWallpaperComponent(infoStage, "kwp"); mLockHasLiveComponent = kwpService != null; boolean separateLockWallpaper = mLockHasLiveComponent || lockImageStage.exists(); // if there's no separate lock wallpaper, apply the system wallpaper to both screens. final int sysWhich = separateLockWallpaper ? FLAG_SYSTEM : FLAG_SYSTEM | FLAG_LOCK; // It is valid for the imagery to be absent; it means that we were not permitted // to back up the original image on the source device, or there was no user-supplied // wallpaper image present. if (lockImageStageExists) { restoreFromStage(lockImageStage, infoStage, "kwp", FLAG_LOCK, sourceDeviceDimensions); } restoreFromStage(imageStage, infoStage, "wp", sysWhich, sourceDeviceDimensions); // And reset to the wallpaper service we should be using if (mLockHasLiveComponent) { updateWallpaperComponent(kwpService, FLAG_LOCK); } updateWallpaperComponent(wpService, sysWhich); } catch (Exception e) { Slog.e(TAG, "Unable to restore wallpaper: " + e.getMessage()); mEventLogger.onRestoreException(e); } finally { Slog.v(TAG, "Restore finished; clearing backup bookkeeping"); infoStage.delete(); imageStage.delete(); lockImageStage.delete(); deviceDimensionsStage.delete(); SharedPreferences prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); prefs.edit() .putInt(SYSTEM_GENERATION, -1) .putInt(LOCK_GENERATION, -1) .commit(); } } /** * This method parses the given file for the backed up device dimensions * * @param deviceDimensions the file which holds the device dimensions * @return the backed up device dimensions */ private Pair parseDeviceDimensions(File deviceDimensions) { int width = 0, height = 0, secondaryHeight = 0, secondaryWidth = 0; try { TypedXmlPullParser parser = Xml.resolvePullParser( new FileInputStream(deviceDimensions)); while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() != XmlPullParser.START_TAG) { continue; } String name = parser.getName(); switch (name) { case "width": String widthText = readText(parser); width = Integer.valueOf(widthText); break; case "height": String textHeight = readText(parser); height = Integer.valueOf(textHeight); break; case "secondarywidth": String secondaryWidthText = readText(parser); secondaryWidth = Integer.valueOf(secondaryWidthText); break; case "secondaryheight": String secondaryHeightText = readText(parser); secondaryHeight = Integer.valueOf(secondaryHeightText); break; default: break; } } return new Pair<>(new Point(width, height), new Point(secondaryWidth, secondaryHeight)); } catch (Exception e) { return null; } } @VisibleForTesting void updateWallpaperComponent(ComponentName wpService, int which) throws IOException { if (servicePackageExists(wpService)) { Slog.i(TAG, "Using wallpaper service " + wpService); mWallpaperManager.setWallpaperComponentWithFlags(wpService, which); if ((which & FLAG_LOCK) != 0) { mEventLogger.onLockLiveWallpaperRestored(wpService); } if ((which & FLAG_SYSTEM) != 0) { mEventLogger.onSystemLiveWallpaperRestored(wpService); } } else { // If we've restored a live wallpaper, but the component doesn't exist, // we should log it as an error so we can easily identify the problem // in reports from users if (wpService != null) { // TODO(b/268471749): Handle delayed case applyComponentAtInstall(wpService, which); Slog.w(TAG, "Wallpaper service " + wpService + " isn't available. " + " Will try to apply later"); } } } private void restoreFromStage(File stage, File info, String hintTag, int which, Pair sourceDeviceDimensions) throws IOException { if (stage.exists()) { if (multiCrop()) { // TODO(b/332937943): implement offset adjustment by manually adjusting crop to // adhere to device aspect ratio SparseArray cropHints = parseCropHints(info, hintTag); if (cropHints != null) { Slog.i(TAG, "Got restored wallpaper; applying which=" + which + "; cropHints = " + cropHints); try (FileInputStream in = new FileInputStream(stage)) { mWallpaperManager.setStreamWithCrops(in, cropHints, true, which); } // And log the success if ((which & FLAG_SYSTEM) > 0) { mEventLogger.onSystemImageWallpaperRestored(); } if ((which & FLAG_LOCK) > 0) { mEventLogger.onLockImageWallpaperRestored(); } } else { logRestoreError(which, ERROR_NO_METADATA); } return; } // Parse the restored info file to find the crop hint. Note that this currently // relies on a priori knowledge of the wallpaper info file schema. Rect cropHint = parseCropHint(info, hintTag); if (cropHint != null) { Slog.i(TAG, "Got restored wallpaper; applying which=" + which + "; cropHint = " + cropHint); try (FileInputStream in = new FileInputStream(stage)) { if (sourceDeviceDimensions != null && sourceDeviceDimensions.first != null) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; ParcelFileDescriptor pdf = ParcelFileDescriptor.open(stage, MODE_READ_ONLY); BitmapFactory.decodeFileDescriptor(pdf.getFileDescriptor(), null, options); Point bitmapSize = new Point(options.outWidth, options.outHeight); Point sourceDeviceSize = new Point(sourceDeviceDimensions.first.x, sourceDeviceDimensions.first.y); Point targetDeviceDimensions = getScreenDimensions(); // TODO: for now we handle only the case where the target device has smaller // aspect ratio than the source device i.e. the target device is more narrow // than the source device if (isTargetMoreNarrowThanSource(targetDeviceDimensions, sourceDeviceSize)) { Rect adjustedCrop = findNewCropfromOldCrop(cropHint, sourceDeviceDimensions.first, true, targetDeviceDimensions, bitmapSize, true); cropHint.set(adjustedCrop); } } mWallpaperManager.setStream(in, cropHint.isEmpty() ? null : cropHint, true, which); // And log the success if ((which & FLAG_SYSTEM) > 0) { mEventLogger.onSystemImageWallpaperRestored(); } if ((which & FLAG_LOCK) > 0) { mEventLogger.onLockImageWallpaperRestored(); } } } else { logRestoreError(which, ERROR_NO_METADATA); } } else { Slog.d(TAG, "Restore data doesn't exist for file " + stage.getPath()); logRestoreErrorIfNoLiveComponent(which, ERROR_NO_WALLPAPER); } } /** * This method computes the crop of the stored wallpaper to preserve its center point as the * user had set it in the previous device. * * The algorithm involves first computing the original crop of the user (without parallax). Then * manually adjusting the user's original crop to respect the current device's aspect ratio * (thereby preserving the center point). Then finally, adding any leftover image real-estate * (i.e. space left over on the horizontal axis) to add parallax effect. Parallax is only added * if was present in the old device's settings. * */ private Rect findNewCropfromOldCrop(Rect oldCrop, Point oldDisplaySize, boolean oldRtl, Point newDisplaySize, Point bitmapSize, boolean newRtl) { Rect cropWithoutParallax = withoutParallax(oldCrop, oldDisplaySize, oldRtl, bitmapSize); oldCrop = oldCrop.isEmpty() ? new Rect(0, 0, bitmapSize.x, bitmapSize.y) : oldCrop; float oldParallaxAmount = ((float) oldCrop.width() / cropWithoutParallax.width()) - 1; Rect newCropWithSameCenterWithoutParallax = sameCenter(newDisplaySize, bitmapSize, cropWithoutParallax); Rect newCrop = newCropWithSameCenterWithoutParallax; // calculate the amount of left-over space there is in the image after adjusting the crop // from the above operation i.e. in a rtl configuration, this is the remaining space in the // image after subtracting the new crop's right edge coordinate from the image itself, and // for ltr, its just the new crop's left edge coordinate (as it's the distance from the // beginning of the image) int widthAvailableForParallaxOnTheNewDevice = (newRtl) ? newCrop.left : bitmapSize.x - newCrop.right; // calculate relatively how much this available space is as a fraction of the total cropped // image float availableParallaxAmount = (float) widthAvailableForParallaxOnTheNewDevice / newCrop.width(); float minAcceptableParallax = Math.min(DEFAULT_ACCEPTABLE_PARALLAX, oldParallaxAmount); if (DEBUG) { Slog.d(TAG, "- cropWithoutParallax: " + cropWithoutParallax); Slog.d(TAG, "- oldParallaxAmount: " + oldParallaxAmount); Slog.d(TAG, "- newCropWithSameCenterWithoutParallax: " + newCropWithSameCenterWithoutParallax); Slog.d(TAG, "- widthAvailableForParallaxOnTheNewDevice: " + widthAvailableForParallaxOnTheNewDevice); Slog.d(TAG, "- availableParallaxAmount: " + availableParallaxAmount); Slog.d(TAG, "- minAcceptableParallax: " + minAcceptableParallax); Slog.d(TAG, "- oldCrop: " + oldCrop); Slog.d(TAG, "- oldDisplaySize: " + oldDisplaySize); Slog.d(TAG, "- oldRtl: " + oldRtl); Slog.d(TAG, "- newDisplaySize: " + newDisplaySize); Slog.d(TAG, "- bitmapSize: " + bitmapSize); Slog.d(TAG, "- newRtl: " + newRtl); } if (availableParallaxAmount >= minAcceptableParallax) { // but in any case, don't put more parallax than the amount of the old device float parallaxToAdd = Math.min(availableParallaxAmount, oldParallaxAmount); int widthToAddForParallax = (int) (newCrop.width() * parallaxToAdd); if (DEBUG) { Slog.d(TAG, "- parallaxToAdd: " + parallaxToAdd); Slog.d(TAG, "- widthToAddForParallax: " + widthToAddForParallax); } if (newRtl) { newCrop.left -= widthToAddForParallax; } else { newCrop.right += widthToAddForParallax; } } return newCrop; } /** * This method computes the original crop of the user without parallax. * * NOTE: When the user sets the wallpaper with a specific crop, there may additional image added * to the crop to support parallax. In order to determine the user's actual crop the parallax * must be removed if it exists. */ Rect withoutParallax(Rect crop, Point displaySize, boolean rtl, Point bitmapSize) { // in the case an image's crop is not set, we assume the image itself is cropped if (crop.isEmpty()) { crop = new Rect(0, 0, bitmapSize.x, bitmapSize.y); } if (DEBUG) { Slog.w(TAG, "- crop: " + crop); } Rect adjustedCrop = new Rect(crop); float suggestedDisplayRatio = (float) displaySize.x / displaySize.y; // here we calculate the width of the wallpaper image such that it has the same aspect ratio // as the given display i.e. the width of the image on a single page of the device without // parallax (i.e. displaySize will correspond to the display the crop was originally set on) int wallpaperWidthWithoutParallax = (int) (0.5f + (float) displaySize.x * crop.height() / displaySize.y); // subtracting wallpaperWidthWithoutParallax from the wallpaper crop gives the amount of // parallax added int widthToRemove = Math.max(0, crop.width() - wallpaperWidthWithoutParallax); if (DEBUG) { Slog.d(TAG, "- adjustedCrop: " + adjustedCrop); Slog.d(TAG, "- suggestedDisplayRatio: " + suggestedDisplayRatio); Slog.d(TAG, "- wallpaperWidthWithoutParallax: " + wallpaperWidthWithoutParallax); Slog.d(TAG, "- widthToRemove: " + widthToRemove); } if (rtl) { adjustedCrop.left += widthToRemove; } else { adjustedCrop.right -= widthToRemove; } if (DEBUG) { Slog.d(TAG, "- adjustedCrop: " + crop); } return adjustedCrop; } /** * This method computes a new crop based on the given crop in order to preserve the center point * of the given crop on the provided displaySize. This is only for the case where the device * displaySize has a smaller aspect ratio than the cropped image. * * NOTE: If the width to height ratio is less in the device display than cropped image * this means the aspect ratios are off and there will be distortions in the image * if the image is applied to the current display (i.e. the image will be skewed -> * pixels in the image will not align correctly with the same pixels in the image that are * above them) */ Rect sameCenter(Point displaySize, Point bitmapSize, Rect crop) { // in the case an image's crop is not set, we assume the image itself is cropped if (crop.isEmpty()) { crop = new Rect(0, 0, bitmapSize.x, bitmapSize.y); } float screenRatio = (float) displaySize.x / displaySize.y; float cropRatio = (float) crop.width() / crop.height(); Rect adjustedCrop = new Rect(crop); if (screenRatio < cropRatio) { // the screen is more narrow than the image, and as such, the image will need to be // zoomed in till it fits in the vertical axis. Due to this, we need to manually adjust // the image's crop in order for it to fit into the screen without having the framework // do it (since the framework left aligns the image after zooming) // Calculate the height of the adjusted wallpaper crop so it respects the aspect ratio // of the device. To calculate the height, we will use the width of the current crop. // This is so we find the largest height possible which also respects the device aspect // ratio. int heightToAdd = (int) (0.5f + crop.width() / screenRatio - crop.height()); // Calculate how much extra image space available that can be used to adjust // the crop. If this amount is less than heightToAdd, from above, then that means we // can't use heightToAdd. Instead we will need to use the maximum possible height, which // is the height of the original bitmap. NOTE: the bitmap height may be different than // the crop. // since there is no guarantee to have height available on both sides // (e.g. the available height might be fully at the bottom), grab the minimum int availableHeight = 2 * Math.min(crop.top, bitmapSize.y - crop.bottom); int actualHeightToAdd = Math.min(heightToAdd, availableHeight); // half of the additional height is added to the top and bottom of the crop adjustedCrop.top -= actualHeightToAdd / 2 + actualHeightToAdd % 2; adjustedCrop.bottom += actualHeightToAdd / 2; // Calculate the width of the adjusted crop. Initially we used the fixed width of the // crop to calculate the heightToAdd, but since this height may be invalid (based on // the calculation above) we calculate the width again instead of using the fixed width, // using the adjustedCrop's updated height. int widthToRemove = (int) (0.5f + crop.width() - adjustedCrop.height() * screenRatio); // half of the additional width is subtracted from the left and right side of the crop int widthToRemoveLeft = widthToRemove / 2; int widthToRemoveRight = widthToRemove / 2 + widthToRemove % 2; adjustedCrop.left += widthToRemoveLeft; adjustedCrop.right -= widthToRemoveRight; if (DEBUG) { Slog.d(TAG, "cropRatio: " + cropRatio); Slog.d(TAG, "screenRatio: " + screenRatio); Slog.d(TAG, "heightToAdd: " + heightToAdd); Slog.d(TAG, "actualHeightToAdd: " + actualHeightToAdd); Slog.d(TAG, "availableHeight: " + availableHeight); Slog.d(TAG, "widthToRemove: " + widthToRemove); Slog.d(TAG, "adjustedCrop: " + adjustedCrop); } return adjustedCrop; } return adjustedCrop; } private boolean isTargetMoreNarrowThanSource(Point targetDisplaySize, Point srcDisplaySize) { float targetScreenRatio = (float) targetDisplaySize.x / targetDisplaySize.y; float srcScreenRatio = (float) srcDisplaySize.x / srcDisplaySize.y; return (targetScreenRatio < srcScreenRatio); } private void logRestoreErrorIfNoLiveComponent(int which, String error) { if (mSystemHasLiveComponent) { return; } logRestoreError(which, error); } private void logRestoreError(int which, String error) { if ((which & FLAG_SYSTEM) == FLAG_SYSTEM) { mEventLogger.onSystemImageWallpaperRestoreFailed(error); } if ((which & FLAG_LOCK) == FLAG_LOCK) { mEventLogger.onLockImageWallpaperRestoreFailed(error); } } private Rect parseCropHint(File wallpaperInfo, String sectionTag) { Rect cropHint = new Rect(); try (FileInputStream stream = new FileInputStream(wallpaperInfo)) { XmlPullParser parser = Xml.resolvePullParser(stream); int type; do { type = parser.next(); if (type == XmlPullParser.START_TAG) { String tag = parser.getName(); if (sectionTag.equals(tag)) { cropHint.left = getAttributeInt(parser, "cropLeft", 0); cropHint.top = getAttributeInt(parser, "cropTop", 0); cropHint.right = getAttributeInt(parser, "cropRight", 0); cropHint.bottom = getAttributeInt(parser, "cropBottom", 0); } } } while (type != XmlPullParser.END_DOCUMENT); } catch (Exception e) { // Whoops; can't process the info file at all. Report failure. Slog.w(TAG, "Failed to parse restored crop: " + e.getMessage()); return null; } return cropHint; } private SparseArray parseCropHints(File wallpaperInfo, String sectionTag) { SparseArray cropHints = new SparseArray<>(); try (FileInputStream stream = new FileInputStream(wallpaperInfo)) { XmlPullParser parser = Xml.resolvePullParser(stream); int type; do { type = parser.next(); if (type != XmlPullParser.START_TAG) continue; String tag = parser.getName(); if (!sectionTag.equals(tag)) continue; for (Pair pair : List.of( new Pair<>(WallpaperManager.PORTRAIT, "Portrait"), new Pair<>(WallpaperManager.LANDSCAPE, "Landscape"), new Pair<>(WallpaperManager.SQUARE_PORTRAIT, "SquarePortrait"), new Pair<>(WallpaperManager.SQUARE_LANDSCAPE, "SquareLandscape"))) { Rect cropHint = new Rect( getAttributeInt(parser, "cropLeft" + pair.second, 0), getAttributeInt(parser, "cropTop" + pair.second, 0), getAttributeInt(parser, "cropRight" + pair.second, 0), getAttributeInt(parser, "cropBottom" + pair.second, 0)); if (!cropHint.isEmpty()) cropHints.put(pair.first, cropHint); } if (cropHints.size() == 0) { // migration case: the crops per screen orientation are not specified. // use the old attributes to restore the crop for one screen orientation. Rect cropHint = new Rect( getAttributeInt(parser, "cropLeft", 0), getAttributeInt(parser, "cropTop", 0), getAttributeInt(parser, "cropRight", 0), getAttributeInt(parser, "cropBottom", 0)); if (!cropHint.isEmpty()) cropHints.put(ORIENTATION_UNKNOWN, cropHint); } } while (type != XmlPullParser.END_DOCUMENT); } catch (Exception e) { // Whoops; can't process the info file at all. Report failure. Slog.w(TAG, "Failed to parse restored crops: " + e.getMessage()); return null; } return cropHints; } private ComponentName parseWallpaperComponent(File wallpaperInfo, String sectionTag) { ComponentName name = null; try (FileInputStream stream = new FileInputStream(wallpaperInfo)) { final XmlPullParser parser = Xml.resolvePullParser(stream); int type; do { type = parser.next(); if (type == XmlPullParser.START_TAG) { String tag = parser.getName(); if (sectionTag.equals(tag)) { final String parsedName = parser.getAttributeValue(null, "component"); name = (parsedName != null) ? ComponentName.unflattenFromString(parsedName) : null; break; } } } while (type != XmlPullParser.END_DOCUMENT); } catch (Exception e) { // Whoops; can't process the info file at all. Report failure. Slog.w(TAG, "Failed to parse restored component: " + e.getMessage()); return null; } return name; } private int getAttributeInt(XmlPullParser parser, String name, int defValue) { final String value = parser.getAttributeValue(null, name); return (value == null) ? defValue : Integer.parseInt(value); } @VisibleForTesting boolean servicePackageExists(ComponentName comp) { try { if (comp != null) { final IPackageManager pm = AppGlobals.getPackageManager(); final PackageInfo info = pm.getPackageInfo(comp.getPackageName(), 0, getUserId()); return (info != null); } } catch (RemoteException e) { Slog.e(TAG, "Unable to contact package manager"); } return false; } /** Unused Key/Value API. */ @Override public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) throws IOException { // Intentionally blank } /** Unused Key/Value API. */ @Override public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) throws IOException { // Intentionally blank } private void applyComponentAtInstall(ComponentName componentName, int which) { PackageMonitor packageMonitor = getWallpaperPackageMonitor( componentName, which); packageMonitor.register(getBaseContext(), null, UserHandle.ALL, true); } @VisibleForTesting PackageMonitor getWallpaperPackageMonitor(ComponentName componentName, int which) { return new PackageMonitor() { @Override public void onPackageAdded(String packageName, int uid) { if (!isDeviceInRestore()) { // We don't want to reapply the wallpaper outside a restore. unregister(); // We have finished restore and not succeeded, so let's log that as an error. WallpaperEventLogger logger = new WallpaperEventLogger( mBackupManager.getDelayedRestoreLogger()); if ((which & FLAG_SYSTEM) != 0) { logger.onSystemLiveWallpaperRestoreFailed( WallpaperEventLogger.ERROR_LIVE_PACKAGE_NOT_INSTALLED); } if ((which & FLAG_LOCK) != 0) { logger.onLockLiveWallpaperRestoreFailed( WallpaperEventLogger.ERROR_LIVE_PACKAGE_NOT_INSTALLED); } mBackupManager.reportDelayedRestoreResult(logger.getBackupRestoreLogger()); return; } if (componentName.getPackageName().equals(packageName)) { Slog.d(TAG, "Applying component " + componentName); boolean success = mWallpaperManager.setWallpaperComponentWithFlags( componentName, which); WallpaperEventLogger logger = new WallpaperEventLogger( mBackupManager.getDelayedRestoreLogger()); if (success) { if ((which & FLAG_SYSTEM) != 0) { logger.onSystemLiveWallpaperRestored(componentName); } if ((which & FLAG_LOCK) != 0) { logger.onLockLiveWallpaperRestored(componentName); } } else { if ((which & FLAG_SYSTEM) != 0) { logger.onSystemLiveWallpaperRestoreFailed( WallpaperEventLogger.ERROR_SET_COMPONENT_EXCEPTION); } if ((which & FLAG_LOCK) != 0) { logger.onLockLiveWallpaperRestoreFailed( WallpaperEventLogger.ERROR_SET_COMPONENT_EXCEPTION); } } // We're only expecting to restore the wallpaper component once. unregister(); mBackupManager.reportDelayedRestoreResult(logger.getBackupRestoreLogger()); } } }; } /** * This method retrieves the dimensions of the largest display of the device * * @return a @{Point} object that contains the dimensions of the largest display on the device */ private Point getScreenDimensions() { Point largetDimensions = null; int maxArea = 0; for (Display display : getInternalDisplays()) { Point displaySize = getRealSize(display); int width = displaySize.x; int height = displaySize.y; int area = width * height; if (area > maxArea) { maxArea = area; largetDimensions = displaySize; } } return largetDimensions; } private Point getRealSize(Display display) { DisplayInfo displayInfo = new DisplayInfo(); display.getDisplayInfo(displayInfo); return new Point(displayInfo.logicalWidth, displayInfo.logicalHeight); } /** * This method returns the smaller display on a multi-display device * * @return Display that corresponds to the smaller display on a device or null if ther is only * one Display on a device */ private Display getSmallerDisplayIfExists() { List internalDisplays = getInternalDisplays(); Point largestDisplaySize = getScreenDimensions(); // Find the first non-matching internal display for (Display display : internalDisplays) { Point displaySize = getRealSize(display); if (displaySize.x != largestDisplaySize.x || displaySize.y != largestDisplaySize.y) { return display; } } // If no smaller display found, return null, as there is only a single display return null; } /** * This method retrieves the collection of Display objects available in the device. * i.e. non-external displays are ignored * * @return list of displays corresponding to each display in the device */ private List getInternalDisplays() { Display[] allDisplays = mDisplayManager.getDisplays( DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED); List internalDisplays = new ArrayList<>(); for (Display display : allDisplays) { if (display.getType() == Display.TYPE_INTERNAL) { internalDisplays.add(display); } } return internalDisplays; } @VisibleForTesting boolean isDeviceInRestore() { try { boolean isInSetup = Settings.Secure.getInt(getBaseContext().getContentResolver(), Settings.Secure.USER_SETUP_COMPLETE) == 0; boolean isInDeferredSetup = Settings.Secure.getInt(getBaseContext() .getContentResolver(), Settings.Secure.USER_SETUP_PERSONALIZATION_STATE) == Settings.Secure.USER_SETUP_PERSONALIZATION_STARTED; return isInSetup || isInDeferredSetup; } catch (Settings.SettingNotFoundException e) { Slog.w(TAG, "Failed to check if the user is in restore: " + e); return false; } } @VisibleForTesting void setBackupManagerForTesting(BackupManager backupManager) { mBackupManager = backupManager; } }