• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.wallpaperbackup;
18 
19 import static android.app.WallpaperManager.FLAG_LOCK;
20 import static android.app.WallpaperManager.FLAG_SYSTEM;
21 import static android.app.WallpaperManager.ORIENTATION_UNKNOWN;
22 import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
23 
24 import static com.android.wallpaperbackup.WallpaperEventLogger.ERROR_INELIGIBLE;
25 import static com.android.wallpaperbackup.WallpaperEventLogger.ERROR_NO_METADATA;
26 import static com.android.wallpaperbackup.WallpaperEventLogger.ERROR_NO_WALLPAPER;
27 import static com.android.wallpaperbackup.WallpaperEventLogger.ERROR_QUOTA_EXCEEDED;
28 import static com.android.window.flags.Flags.multiCrop;
29 
30 import android.app.AppGlobals;
31 import android.app.WallpaperManager;
32 import android.app.backup.BackupAgent;
33 import android.app.backup.BackupDataInput;
34 import android.app.backup.BackupDataOutput;
35 import android.app.backup.BackupManager;
36 import android.app.backup.BackupRestoreEventLogger.BackupRestoreError;
37 import android.app.backup.FullBackupDataOutput;
38 import android.content.ComponentName;
39 import android.content.Context;
40 import android.content.SharedPreferences;
41 import android.content.pm.IPackageManager;
42 import android.content.pm.PackageInfo;
43 import android.graphics.BitmapFactory;
44 import android.graphics.Point;
45 import android.graphics.Rect;
46 import android.hardware.display.DisplayManager;
47 import android.os.FileUtils;
48 import android.os.ParcelFileDescriptor;
49 import android.os.RemoteException;
50 import android.os.UserHandle;
51 import android.provider.Settings;
52 import android.util.Pair;
53 import android.util.Slog;
54 import android.util.SparseArray;
55 import android.util.Xml;
56 import android.view.Display;
57 import android.view.DisplayInfo;
58 
59 import com.android.internal.annotations.VisibleForTesting;
60 import com.android.internal.content.PackageMonitor;
61 import com.android.modules.utils.TypedXmlPullParser;
62 import com.android.modules.utils.TypedXmlSerializer;
63 
64 import org.xmlpull.v1.XmlPullParser;
65 import org.xmlpull.v1.XmlPullParserException;
66 
67 import java.io.File;
68 import java.io.FileInputStream;
69 import java.io.FileOutputStream;
70 import java.io.IOException;
71 import java.util.ArrayList;
72 import java.util.List;
73 
74 /**
75  * Backs up and restores wallpaper and metadata related to it.
76  *
77  * This agent has its own package because it does full backup as opposed to SystemBackupAgent
78  * which does key/value backup.
79  *
80  * This class stages wallpaper files for backup by copying them into its own directory because of
81  * the following reasons:
82  *
83  * <ul>
84  *     <li>Non-system users don't have permission to read the directory that the system stores
85  *     the wallpaper files in</li>
86  *     <li>{@link BackupAgent} enforces that backed up files must live inside the package's
87  *     {@link Context#getFilesDir()}</li>
88  * </ul>
89  *
90  * There are 3 files to back up:
91  * <ul>
92  *     <li>The "wallpaper info"  file which contains metadata like the crop applied to the
93  *     wallpaper or the live wallpaper component name.</li>
94  *     <li>The "system" wallpaper file.</li>
95  *     <li>An optional "lock" wallpaper, which is shown on the lockscreen instead of the system
96  *     wallpaper if set.</li>
97  * </ul>
98  *
99  * On restore, the metadata file is parsed and {@link WallpaperManager} APIs are used to set the
100  * wallpaper. Note that if there's a live wallpaper, the live wallpaper package name will be
101  * part of the metadata file and the wallpaper will be applied when the package it's installed.
102  */
103 public class WallpaperBackupAgent extends BackupAgent {
104     private static final String TAG = "WallpaperBackup";
105     private static final boolean DEBUG = false;
106 
107     // Names of our local-data stage files
108     @VisibleForTesting
109     static final String SYSTEM_WALLPAPER_STAGE = "wallpaper-stage";
110     @VisibleForTesting
111     static final String LOCK_WALLPAPER_STAGE = "wallpaper-lock-stage";
112     @VisibleForTesting
113     static final String WALLPAPER_INFO_STAGE = "wallpaper-info-stage";
114     @VisibleForTesting
115     static final String WALLPAPER_BACKUP_DEVICE_INFO_STAGE = "wallpaper-backup-device-info-stage";
116     static final String EMPTY_SENTINEL = "empty";
117     static final String QUOTA_SENTINEL = "quota";
118     // Shared preferences constants.
119     static final String PREFS_NAME = "wbprefs.xml";
120     static final String SYSTEM_GENERATION = "system_gen";
121     static final String LOCK_GENERATION = "lock_gen";
122 
123     static final float DEFAULT_ACCEPTABLE_PARALLAX = 0.2f;
124 
125     // If this file exists, it means we exceeded our quota last time
126     private File mQuotaFile;
127     private boolean mQuotaExceeded;
128 
129     private WallpaperManager mWallpaperManager;
130     private WallpaperEventLogger mEventLogger;
131     private BackupManager mBackupManager;
132 
133     private boolean mSystemHasLiveComponent;
134     private boolean mLockHasLiveComponent;
135 
136     private DisplayManager mDisplayManager;
137 
138     @Override
onCreate()139     public void onCreate() {
140         if (DEBUG) {
141             Slog.v(TAG, "onCreate()");
142         }
143 
144         mWallpaperManager = getSystemService(WallpaperManager.class);
145 
146         mQuotaFile = new File(getFilesDir(), QUOTA_SENTINEL);
147         mQuotaExceeded = mQuotaFile.exists();
148         if (DEBUG) {
149             Slog.v(TAG, "quota file " + mQuotaFile.getPath() + " exists=" + mQuotaExceeded);
150         }
151 
152         mBackupManager = new BackupManager(getBaseContext());
153         mEventLogger = new WallpaperEventLogger(mBackupManager, /* wallpaperAgent */ this);
154 
155         mDisplayManager = getSystemService(DisplayManager.class);
156     }
157 
158     @Override
onFullBackup(FullBackupDataOutput data)159     public void onFullBackup(FullBackupDataOutput data) throws IOException {
160         try {
161             // We always back up this 'empty' file to ensure that the absence of
162             // storable wallpaper imagery still produces a non-empty backup data
163             // stream, otherwise it'd simply be ignored in preflight.
164             final File empty = new File(getFilesDir(), EMPTY_SENTINEL);
165             if (!empty.exists()) {
166                 FileOutputStream touch = new FileOutputStream(empty);
167                 touch.close();
168             }
169             backupFile(empty, data);
170 
171             SharedPreferences sharedPrefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
172 
173             // Check the IDs of the wallpapers that we backed up last time. If they haven't
174             // changed, we won't re-stage them for backup and use the old staged versions to avoid
175             // disk churn.
176             final int lastSysGeneration = sharedPrefs.getInt(SYSTEM_GENERATION, /* defValue= */ -1);
177             final int lastLockGeneration = sharedPrefs.getInt(LOCK_GENERATION, /* defValue= */ -1);
178             final int sysGeneration = mWallpaperManager.getWallpaperId(FLAG_SYSTEM);
179             final int lockGeneration = mWallpaperManager.getWallpaperId(FLAG_LOCK);
180             final boolean sysChanged = (sysGeneration != lastSysGeneration);
181             final boolean lockChanged = (lockGeneration != lastLockGeneration);
182 
183             if (DEBUG) {
184                 Slog.v(TAG, "sysGen=" + sysGeneration + " : sysChanged=" + sysChanged);
185                 Slog.v(TAG, "lockGen=" + lockGeneration + " : lockChanged=" + lockChanged);
186             }
187 
188             // Due to the way image vs live wallpaper backup logic is intermingled, for logging
189             // purposes first check if we have live components for each wallpaper to avoid
190             // over-reporting errors.
191             mSystemHasLiveComponent = mWallpaperManager.getWallpaperInfo(FLAG_SYSTEM) != null;
192             mLockHasLiveComponent = mWallpaperManager.getWallpaperInfo(FLAG_LOCK) != null;
193 
194             // performing backup of each file based on order of importance
195             backupWallpaperInfoFile(/* sysOrLockChanged= */ sysChanged || lockChanged, data);
196             backupSystemWallpaperFile(sharedPrefs, sysChanged, sysGeneration, data);
197             backupLockWallpaperFileIfItExists(sharedPrefs, lockChanged, lockGeneration, data);
198             backupDeviceInfoFile(data);
199         } catch (Exception e) {
200             Slog.e(TAG, "Unable to back up wallpaper", e);
201             mEventLogger.onBackupException(e);
202         } finally {
203             // Even if this time we had to back off on attempting to store the lock image
204             // due to exceeding the data quota, try again next time.  This will alternate
205             // between "try both" and "only store the primary image" until either there
206             // is no lock image to store, or the quota is raised, or both fit under the
207             // quota.
208             mQuotaFile.delete();
209         }
210     }
211 
212     /**
213      * This method backs up the device dimension information. The device data will always get
214      * overwritten when triggering a backup
215      */
backupDeviceInfoFile(FullBackupDataOutput data)216     private void backupDeviceInfoFile(FullBackupDataOutput data)
217             throws IOException {
218         final File deviceInfoStage = new File(getFilesDir(), WALLPAPER_BACKUP_DEVICE_INFO_STAGE);
219 
220         // save the dimensions of the device with xml formatting
221         Point dimensions = getScreenDimensions();
222         Display smallerDisplay = getSmallerDisplayIfExists();
223         Point secondaryDimensions = smallerDisplay != null ? getRealSize(smallerDisplay) :
224                 new Point(0, 0);
225 
226         deviceInfoStage.createNewFile();
227         FileOutputStream fstream = new FileOutputStream(deviceInfoStage, false);
228         TypedXmlSerializer out = Xml.resolveSerializer(fstream);
229         out.startDocument(null, true);
230         out.startTag(null, "dimensions");
231 
232         out.startTag(null, "width");
233         out.text(String.valueOf(dimensions.x));
234         out.endTag(null, "width");
235 
236         out.startTag(null, "height");
237         out.text(String.valueOf(dimensions.y));
238         out.endTag(null, "height");
239 
240         if (smallerDisplay != null) {
241             out.startTag(null, "secondarywidth");
242             out.text(String.valueOf(secondaryDimensions.x));
243             out.endTag(null, "secondarywidth");
244 
245             out.startTag(null, "secondaryheight");
246             out.text(String.valueOf(secondaryDimensions.y));
247             out.endTag(null, "secondaryheight");
248         }
249 
250         out.endTag(null, "dimensions");
251         out.endDocument();
252         fstream.flush();
253         FileUtils.sync(fstream);
254         fstream.close();
255 
256         if (DEBUG) Slog.v(TAG, "Storing device dimension data");
257         backupFile(deviceInfoStage, data);
258     }
259 
backupWallpaperInfoFile(boolean sysOrLockChanged, FullBackupDataOutput data)260     private void backupWallpaperInfoFile(boolean sysOrLockChanged, FullBackupDataOutput data)
261             throws IOException {
262         final ParcelFileDescriptor wallpaperInfoFd = mWallpaperManager.getWallpaperInfoFile();
263 
264         if (wallpaperInfoFd == null) {
265             Slog.w(TAG, "Wallpaper metadata file doesn't exist");
266             // If we have live components, getting the file to back up somehow failed, so log it
267             // as an error.
268             if (mSystemHasLiveComponent) {
269                 mEventLogger.onSystemLiveWallpaperBackupFailed(ERROR_NO_METADATA);
270             }
271             if (mLockHasLiveComponent) {
272                 mEventLogger.onLockLiveWallpaperBackupFailed(ERROR_NO_METADATA);
273             }
274             return;
275         }
276 
277         final File infoStage = new File(getFilesDir(), WALLPAPER_INFO_STAGE);
278 
279         if (sysOrLockChanged || !infoStage.exists()) {
280             if (DEBUG) Slog.v(TAG, "New wallpaper configuration; copying");
281             copyFromPfdToFileAndClosePfd(wallpaperInfoFd, infoStage);
282         }
283 
284         if (DEBUG) Slog.v(TAG, "Storing wallpaper metadata");
285         backupFile(infoStage, data);
286 
287         // We've backed up the info file which contains the live component, so log it as success
288         if (mSystemHasLiveComponent) {
289             mEventLogger.onSystemLiveWallpaperBackedUp(
290                     mWallpaperManager.getWallpaperInfo(FLAG_SYSTEM));
291         }
292         if (mLockHasLiveComponent) {
293             mEventLogger.onLockLiveWallpaperBackedUp(mWallpaperManager.getWallpaperInfo(FLAG_LOCK));
294         }
295     }
296 
backupSystemWallpaperFile(SharedPreferences sharedPrefs, boolean sysChanged, int sysGeneration, FullBackupDataOutput data)297     private void backupSystemWallpaperFile(SharedPreferences sharedPrefs,
298             boolean sysChanged, int sysGeneration, FullBackupDataOutput data) throws IOException {
299         if (!mWallpaperManager.isWallpaperBackupEligible(FLAG_SYSTEM)) {
300             Slog.d(TAG, "System wallpaper ineligible for backup");
301             logSystemImageErrorIfNoLiveComponent(ERROR_INELIGIBLE);
302             return;
303         }
304 
305         final ParcelFileDescriptor systemWallpaperImageFd = mWallpaperManager.getWallpaperFile(
306                 FLAG_SYSTEM,
307                 /* getCropped= */ false);
308 
309         if (systemWallpaperImageFd == null) {
310             Slog.w(TAG, "System wallpaper doesn't exist");
311             logSystemImageErrorIfNoLiveComponent(ERROR_NO_WALLPAPER);
312             return;
313         }
314 
315         final File imageStage = new File(getFilesDir(), SYSTEM_WALLPAPER_STAGE);
316 
317         if (sysChanged || !imageStage.exists()) {
318             if (DEBUG) Slog.v(TAG, "New system wallpaper; copying");
319             copyFromPfdToFileAndClosePfd(systemWallpaperImageFd, imageStage);
320         }
321 
322         if (DEBUG) Slog.v(TAG, "Storing system wallpaper image");
323         backupFile(imageStage, data);
324         sharedPrefs.edit().putInt(SYSTEM_GENERATION, sysGeneration).apply();
325         mEventLogger.onSystemImageWallpaperBackedUp();
326     }
327 
logSystemImageErrorIfNoLiveComponent(@ackupRestoreError String error)328     private void logSystemImageErrorIfNoLiveComponent(@BackupRestoreError String error) {
329         if (mSystemHasLiveComponent) {
330             return;
331         }
332         mEventLogger.onSystemImageWallpaperBackupFailed(error);
333     }
334 
backupLockWallpaperFileIfItExists(SharedPreferences sharedPrefs, boolean lockChanged, int lockGeneration, FullBackupDataOutput data)335     private void backupLockWallpaperFileIfItExists(SharedPreferences sharedPrefs,
336             boolean lockChanged, int lockGeneration, FullBackupDataOutput data) throws IOException {
337         final File lockImageStage = new File(getFilesDir(), LOCK_WALLPAPER_STAGE);
338 
339         // This means there's no lock wallpaper set by the user.
340         if (lockGeneration == -1) {
341             if (lockChanged && lockImageStage.exists()) {
342                 if (DEBUG) Slog.v(TAG, "Removed lock wallpaper; deleting");
343                 lockImageStage.delete();
344             }
345             Slog.d(TAG, "No lockscreen wallpaper set, add nothing to backup");
346             sharedPrefs.edit().putInt(LOCK_GENERATION, lockGeneration).apply();
347             logLockImageErrorIfNoLiveComponent(ERROR_NO_WALLPAPER);
348             return;
349         }
350 
351         if (!mWallpaperManager.isWallpaperBackupEligible(FLAG_LOCK)) {
352             Slog.d(TAG, "Lock screen wallpaper ineligible for backup");
353             logLockImageErrorIfNoLiveComponent(ERROR_INELIGIBLE);
354             return;
355         }
356 
357         final ParcelFileDescriptor lockWallpaperFd = mWallpaperManager.getWallpaperFile(
358                 FLAG_LOCK, /* getCropped= */ false);
359 
360         // If we get to this point, that means lockGeneration != -1 so there's a lock wallpaper
361         // set, but we can't find it.
362         if (lockWallpaperFd == null) {
363             Slog.w(TAG, "Lock wallpaper doesn't exist");
364             logLockImageErrorIfNoLiveComponent(ERROR_NO_WALLPAPER);
365             return;
366         }
367 
368         if (mQuotaExceeded) {
369             Slog.w(TAG, "Not backing up lock screen wallpaper. Quota was exceeded last time");
370             logLockImageErrorIfNoLiveComponent(ERROR_QUOTA_EXCEEDED);
371             return;
372         }
373 
374         if (lockChanged || !lockImageStage.exists()) {
375             if (DEBUG) Slog.v(TAG, "New lock wallpaper; copying");
376             copyFromPfdToFileAndClosePfd(lockWallpaperFd, lockImageStage);
377         }
378 
379         if (DEBUG) Slog.v(TAG, "Storing lock wallpaper image");
380         backupFile(lockImageStage, data);
381         sharedPrefs.edit().putInt(LOCK_GENERATION, lockGeneration).apply();
382         mEventLogger.onLockImageWallpaperBackedUp();
383     }
384 
logLockImageErrorIfNoLiveComponent(@ackupRestoreError String error)385     private void logLockImageErrorIfNoLiveComponent(@BackupRestoreError String error) {
386         if (mLockHasLiveComponent) {
387             return;
388         }
389         mEventLogger.onLockImageWallpaperBackupFailed(error);
390     }
391 
392     /**
393      * Copies the contents of the given {@code pfd} to the given {@code file}.
394      *
395      * All resources used in the process including the {@code pfd} will be closed.
396      */
copyFromPfdToFileAndClosePfd(ParcelFileDescriptor pfd, File file)397     private static void copyFromPfdToFileAndClosePfd(ParcelFileDescriptor pfd, File file)
398             throws IOException {
399         try (ParcelFileDescriptor.AutoCloseInputStream inputStream =
400                      new ParcelFileDescriptor.AutoCloseInputStream(pfd);
401              FileOutputStream outputStream = new FileOutputStream(file)
402         ) {
403             FileUtils.copy(inputStream, outputStream);
404         }
405     }
406 
readText(TypedXmlPullParser parser)407     private static String readText(TypedXmlPullParser parser)
408             throws IOException, XmlPullParserException {
409         String result = "";
410         if (parser.next() == XmlPullParser.TEXT) {
411             result = parser.getText();
412             parser.nextTag();
413         }
414         return result;
415     }
416 
417     @VisibleForTesting
418     // fullBackupFile is final, so we intercept backups here in tests.
backupFile(File file, FullBackupDataOutput data)419     protected void backupFile(File file, FullBackupDataOutput data) {
420         fullBackupFile(file, data);
421     }
422 
423     @Override
onQuotaExceeded(long backupDataBytes, long quotaBytes)424     public void onQuotaExceeded(long backupDataBytes, long quotaBytes) {
425         Slog.i(TAG, "Quota exceeded (" + backupDataBytes + " vs " + quotaBytes + ')');
426         try (FileOutputStream f = new FileOutputStream(mQuotaFile)) {
427             f.write(0);
428         } catch (Exception e) {
429             Slog.w(TAG, "Unable to record quota-exceeded: " + e.getMessage());
430         }
431     }
432 
433     // We use the default onRestoreFile() implementation that will recreate our stage files,
434     // then post-process in onRestoreFinished() to apply the new wallpaper.
435     @Override
onRestoreFinished()436     public void onRestoreFinished() {
437         Slog.v(TAG, "onRestoreFinished()");
438         final File filesDir = getFilesDir();
439         final File infoStage = new File(filesDir, WALLPAPER_INFO_STAGE);
440         final File imageStage = new File(filesDir, SYSTEM_WALLPAPER_STAGE);
441         final File lockImageStage = new File(filesDir, LOCK_WALLPAPER_STAGE);
442         final File deviceDimensionsStage = new File(filesDir, WALLPAPER_BACKUP_DEVICE_INFO_STAGE);
443         boolean lockImageStageExists = lockImageStage.exists();
444 
445         try {
446             // Parse the device dimensions of the source device
447             Pair<Point, Point> sourceDeviceDimensions = parseDeviceDimensions(
448                     deviceDimensionsStage);
449 
450             // First parse the live component name so that we know for logging if we care about
451             // logging errors with the image restore.
452             ComponentName wpService = parseWallpaperComponent(infoStage, "wp");
453             mSystemHasLiveComponent = wpService != null;
454 
455             ComponentName kwpService = parseWallpaperComponent(infoStage, "kwp");
456             mLockHasLiveComponent = kwpService != null;
457             boolean separateLockWallpaper = mLockHasLiveComponent || lockImageStage.exists();
458 
459             // if there's no separate lock wallpaper, apply the system wallpaper to both screens.
460             final int sysWhich = separateLockWallpaper ? FLAG_SYSTEM : FLAG_SYSTEM | FLAG_LOCK;
461 
462             // It is valid for the imagery to be absent; it means that we were not permitted
463             // to back up the original image on the source device, or there was no user-supplied
464             // wallpaper image present.
465             if (lockImageStageExists) {
466                 restoreFromStage(lockImageStage, infoStage, "kwp", FLAG_LOCK,
467                         sourceDeviceDimensions);
468             }
469             restoreFromStage(imageStage, infoStage, "wp", sysWhich, sourceDeviceDimensions);
470 
471             // And reset to the wallpaper service we should be using
472             if (mLockHasLiveComponent) {
473                 updateWallpaperComponent(kwpService, FLAG_LOCK);
474             }
475             updateWallpaperComponent(wpService, sysWhich);
476         } catch (Exception e) {
477             Slog.e(TAG, "Unable to restore wallpaper: " + e.getMessage());
478             mEventLogger.onRestoreException(e);
479         } finally {
480             Slog.v(TAG, "Restore finished; clearing backup bookkeeping");
481             infoStage.delete();
482             imageStage.delete();
483             lockImageStage.delete();
484             deviceDimensionsStage.delete();
485 
486             SharedPreferences prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
487             prefs.edit()
488                     .putInt(SYSTEM_GENERATION, -1)
489                     .putInt(LOCK_GENERATION, -1)
490                     .commit();
491         }
492     }
493 
494     /**
495      * This method parses the given file for the backed up device dimensions
496      *
497      * @param deviceDimensions the file which holds the device dimensions
498      * @return the backed up device dimensions
499      */
parseDeviceDimensions(File deviceDimensions)500     private Pair<Point, Point> parseDeviceDimensions(File deviceDimensions) {
501         int width = 0, height = 0, secondaryHeight = 0, secondaryWidth = 0;
502         try {
503             TypedXmlPullParser parser = Xml.resolvePullParser(
504                     new FileInputStream(deviceDimensions));
505 
506             while (parser.next() != XmlPullParser.END_TAG) {
507                 if (parser.getEventType() != XmlPullParser.START_TAG) {
508                     continue;
509                 }
510 
511                 String name = parser.getName();
512 
513                 switch (name) {
514                     case "width":
515                         String widthText = readText(parser);
516                         width = Integer.valueOf(widthText);
517                         break;
518 
519                     case "height":
520                         String textHeight = readText(parser);
521                         height = Integer.valueOf(textHeight);
522                         break;
523 
524                     case "secondarywidth":
525                         String secondaryWidthText = readText(parser);
526                         secondaryWidth = Integer.valueOf(secondaryWidthText);
527                         break;
528 
529                     case "secondaryheight":
530                         String secondaryHeightText = readText(parser);
531                         secondaryHeight = Integer.valueOf(secondaryHeightText);
532                         break;
533                     default:
534                         break;
535                 }
536             }
537             return new Pair<>(new Point(width, height), new Point(secondaryWidth, secondaryHeight));
538 
539         } catch (Exception e) {
540             return null;
541         }
542     }
543 
544     @VisibleForTesting
updateWallpaperComponent(ComponentName wpService, int which)545     void updateWallpaperComponent(ComponentName wpService, int which)
546             throws IOException {
547         if (servicePackageExists(wpService)) {
548             Slog.i(TAG, "Using wallpaper service " + wpService);
549             mWallpaperManager.setWallpaperComponentWithFlags(wpService, which);
550             if ((which & FLAG_LOCK) != 0) {
551                 mEventLogger.onLockLiveWallpaperRestored(wpService);
552             }
553             if ((which & FLAG_SYSTEM) != 0) {
554                 mEventLogger.onSystemLiveWallpaperRestored(wpService);
555             }
556         } else {
557             // If we've restored a live wallpaper, but the component doesn't exist,
558             // we should log it as an error so we can easily identify the problem
559             // in reports from users
560             if (wpService != null) {
561                 // TODO(b/268471749): Handle delayed case
562                 applyComponentAtInstall(wpService, which);
563                 Slog.w(TAG, "Wallpaper service " + wpService + " isn't available. "
564                         + " Will try to apply later");
565             }
566         }
567     }
568 
restoreFromStage(File stage, File info, String hintTag, int which, Pair<Point, Point> sourceDeviceDimensions)569     private void restoreFromStage(File stage, File info, String hintTag, int which,
570             Pair<Point, Point> sourceDeviceDimensions)
571             throws IOException {
572         if (stage.exists()) {
573             if (multiCrop()) {
574                 // TODO(b/332937943): implement offset adjustment by manually adjusting crop to
575                 //  adhere to device aspect ratio
576                 SparseArray<Rect> cropHints = parseCropHints(info, hintTag);
577                 if (cropHints != null) {
578                     Slog.i(TAG, "Got restored wallpaper; applying which=" + which
579                             + "; cropHints = " + cropHints);
580                     try (FileInputStream in = new FileInputStream(stage)) {
581                         mWallpaperManager.setStreamWithCrops(in, cropHints, true, which);
582                     }
583                     // And log the success
584                     if ((which & FLAG_SYSTEM) > 0) {
585                         mEventLogger.onSystemImageWallpaperRestored();
586                     }
587                     if ((which & FLAG_LOCK) > 0) {
588                         mEventLogger.onLockImageWallpaperRestored();
589                     }
590                 } else {
591                     logRestoreError(which, ERROR_NO_METADATA);
592                 }
593                 return;
594             }
595             // Parse the restored info file to find the crop hint.  Note that this currently
596             // relies on a priori knowledge of the wallpaper info file schema.
597             Rect cropHint = parseCropHint(info, hintTag);
598             if (cropHint != null) {
599                 Slog.i(TAG, "Got restored wallpaper; applying which=" + which
600                         + "; cropHint = " + cropHint);
601                 try (FileInputStream in = new FileInputStream(stage)) {
602 
603                     if (sourceDeviceDimensions != null && sourceDeviceDimensions.first != null) {
604                         BitmapFactory.Options options = new BitmapFactory.Options();
605                         options.inJustDecodeBounds = true;
606                         ParcelFileDescriptor pdf = ParcelFileDescriptor.open(stage, MODE_READ_ONLY);
607                         BitmapFactory.decodeFileDescriptor(pdf.getFileDescriptor(),
608                                 null, options);
609                         Point bitmapSize = new Point(options.outWidth, options.outHeight);
610                         Point sourceDeviceSize = new Point(sourceDeviceDimensions.first.x,
611                                 sourceDeviceDimensions.first.y);
612                         Point targetDeviceDimensions = getScreenDimensions();
613 
614                         // TODO: for now we handle only the case where the target device has smaller
615                         // aspect ratio than the source device i.e. the target device is more narrow
616                         // than the source device
617                         if (isTargetMoreNarrowThanSource(targetDeviceDimensions,
618                                 sourceDeviceSize)) {
619                             Rect adjustedCrop = findNewCropfromOldCrop(cropHint,
620                                     sourceDeviceDimensions.first, true, targetDeviceDimensions,
621                                     bitmapSize, true);
622 
623                             cropHint.set(adjustedCrop);
624                         }
625                     }
626 
627                     mWallpaperManager.setStream(in, cropHint.isEmpty() ? null : cropHint,
628                             true, which);
629 
630                     // And log the success
631                     if ((which & FLAG_SYSTEM) > 0) {
632                         mEventLogger.onSystemImageWallpaperRestored();
633                     }
634                     if ((which & FLAG_LOCK) > 0) {
635                         mEventLogger.onLockImageWallpaperRestored();
636                     }
637                 }
638             } else {
639                 logRestoreError(which, ERROR_NO_METADATA);
640             }
641         } else {
642             Slog.d(TAG, "Restore data doesn't exist for file " + stage.getPath());
643             logRestoreErrorIfNoLiveComponent(which, ERROR_NO_WALLPAPER);
644         }
645     }
646 
647     /**
648      * This method computes the crop of the stored wallpaper to preserve its center point as the
649      * user had set it in the previous device.
650      *
651      * The algorithm involves first computing the original crop of the user (without parallax). Then
652      * manually adjusting the user's original crop to respect the current device's aspect ratio
653      * (thereby preserving the center point). Then finally, adding any leftover image real-estate
654      * (i.e. space left over on the horizontal axis) to add parallax effect. Parallax is only added
655      * if was present in the old device's settings.
656      *
657      */
findNewCropfromOldCrop(Rect oldCrop, Point oldDisplaySize, boolean oldRtl, Point newDisplaySize, Point bitmapSize, boolean newRtl)658     private Rect findNewCropfromOldCrop(Rect oldCrop, Point oldDisplaySize, boolean oldRtl,
659             Point newDisplaySize, Point bitmapSize, boolean newRtl) {
660         Rect cropWithoutParallax = withoutParallax(oldCrop, oldDisplaySize, oldRtl, bitmapSize);
661         oldCrop = oldCrop.isEmpty() ? new Rect(0, 0, bitmapSize.x, bitmapSize.y) : oldCrop;
662         float oldParallaxAmount = ((float) oldCrop.width() / cropWithoutParallax.width()) - 1;
663 
664         Rect newCropWithSameCenterWithoutParallax = sameCenter(newDisplaySize, bitmapSize,
665                 cropWithoutParallax);
666 
667         Rect newCrop = newCropWithSameCenterWithoutParallax;
668 
669         // calculate the amount of left-over space there is in the image after adjusting the crop
670         // from the above operation i.e. in a rtl configuration, this is the remaining space in the
671         // image after subtracting the new crop's right edge coordinate from the image itself, and
672         // for ltr, its just the new crop's left edge coordinate (as it's the distance from the
673         // beginning of the image)
674         int widthAvailableForParallaxOnTheNewDevice =
675                 (newRtl) ? newCrop.left : bitmapSize.x - newCrop.right;
676 
677         // calculate relatively how much this available space is as a fraction of the total cropped
678         // image
679         float availableParallaxAmount =
680                 (float) widthAvailableForParallaxOnTheNewDevice / newCrop.width();
681 
682         float minAcceptableParallax = Math.min(DEFAULT_ACCEPTABLE_PARALLAX, oldParallaxAmount);
683 
684         if (DEBUG) {
685             Slog.d(TAG, "- cropWithoutParallax: " + cropWithoutParallax);
686             Slog.d(TAG, "- oldParallaxAmount: " + oldParallaxAmount);
687             Slog.d(TAG, "- newCropWithSameCenterWithoutParallax: "
688                     + newCropWithSameCenterWithoutParallax);
689             Slog.d(TAG, "- widthAvailableForParallaxOnTheNewDevice: "
690                     + widthAvailableForParallaxOnTheNewDevice);
691             Slog.d(TAG, "- availableParallaxAmount: " + availableParallaxAmount);
692             Slog.d(TAG, "- minAcceptableParallax: " + minAcceptableParallax);
693             Slog.d(TAG, "- oldCrop: " + oldCrop);
694             Slog.d(TAG, "- oldDisplaySize: " + oldDisplaySize);
695             Slog.d(TAG, "- oldRtl: " + oldRtl);
696             Slog.d(TAG, "- newDisplaySize: " + newDisplaySize);
697             Slog.d(TAG, "- bitmapSize: " + bitmapSize);
698             Slog.d(TAG, "- newRtl: " + newRtl);
699         }
700         if (availableParallaxAmount >= minAcceptableParallax) {
701             // but in any case, don't put more parallax than the amount of the old device
702             float parallaxToAdd = Math.min(availableParallaxAmount, oldParallaxAmount);
703 
704             int widthToAddForParallax = (int) (newCrop.width() * parallaxToAdd);
705             if (DEBUG) {
706                 Slog.d(TAG, "- parallaxToAdd: " + parallaxToAdd);
707                 Slog.d(TAG, "- widthToAddForParallax: " + widthToAddForParallax);
708             }
709             if (newRtl) {
710                 newCrop.left -= widthToAddForParallax;
711             } else {
712                 newCrop.right += widthToAddForParallax;
713             }
714         }
715         return newCrop;
716     }
717 
718     /**
719      * This method computes the original crop of the user without parallax.
720      *
721      * NOTE: When the user sets the wallpaper with a specific crop, there may additional image added
722      * to the crop to support parallax. In order to determine the user's actual crop the parallax
723      * must be removed if it exists.
724      */
withoutParallax(Rect crop, Point displaySize, boolean rtl, Point bitmapSize)725     Rect withoutParallax(Rect crop, Point displaySize, boolean rtl, Point bitmapSize) {
726         // in the case an image's crop is not set, we assume the image itself is cropped
727         if (crop.isEmpty()) {
728             crop = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
729         }
730 
731         if (DEBUG) {
732             Slog.w(TAG, "- crop: " + crop);
733         }
734 
735         Rect adjustedCrop = new Rect(crop);
736         float suggestedDisplayRatio = (float) displaySize.x / displaySize.y;
737 
738         // here we calculate the width of the wallpaper image such that it has the same aspect ratio
739         // as the given display i.e. the width of the image on a single page of the device without
740         // parallax (i.e. displaySize will correspond to the display the crop was originally set on)
741         int wallpaperWidthWithoutParallax = (int) (0.5f + (float) displaySize.x * crop.height()
742                 / displaySize.y);
743         // subtracting wallpaperWidthWithoutParallax from the wallpaper crop gives the amount of
744         // parallax added
745         int widthToRemove = Math.max(0, crop.width() - wallpaperWidthWithoutParallax);
746 
747         if (DEBUG) {
748             Slog.d(TAG, "- adjustedCrop: " + adjustedCrop);
749             Slog.d(TAG, "- suggestedDisplayRatio: " + suggestedDisplayRatio);
750             Slog.d(TAG, "- wallpaperWidthWithoutParallax: " + wallpaperWidthWithoutParallax);
751             Slog.d(TAG, "- widthToRemove: " + widthToRemove);
752         }
753         if (rtl) {
754             adjustedCrop.left += widthToRemove;
755         } else {
756             adjustedCrop.right -= widthToRemove;
757         }
758 
759         if (DEBUG) {
760             Slog.d(TAG, "- adjustedCrop: " + crop);
761         }
762         return adjustedCrop;
763     }
764 
765     /**
766      * This method computes a new crop based on the given crop in order to preserve the center point
767      * of the given crop on the provided displaySize. This is only for the case where the device
768      * displaySize has a smaller aspect ratio than the cropped image.
769      *
770      * NOTE: If the width to height ratio is less in the device display than cropped image
771      * this means the aspect ratios are off and there will be distortions in the image
772      * if the image is applied to the current display (i.e. the image will be skewed ->
773      * pixels in the image will not align correctly with the same pixels in the image that are
774      * above them)
775      */
sameCenter(Point displaySize, Point bitmapSize, Rect crop)776     Rect sameCenter(Point displaySize, Point bitmapSize, Rect crop) {
777 
778         // in the case an image's crop is not set, we assume the image itself is cropped
779         if (crop.isEmpty()) {
780             crop = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
781         }
782 
783         float screenRatio = (float) displaySize.x / displaySize.y;
784         float cropRatio = (float) crop.width() / crop.height();
785 
786         Rect adjustedCrop = new Rect(crop);
787 
788         if (screenRatio < cropRatio) {
789             // the screen is more narrow than the image, and as such, the image will need to be
790             // zoomed in till it fits in the vertical axis. Due to this, we need to manually adjust
791             // the image's crop in order for it to fit into the screen without having the framework
792             // do it (since the framework left aligns the image after zooming)
793 
794             // Calculate the height of the adjusted wallpaper crop so it respects the aspect ratio
795             // of the device. To calculate the height, we will use the width of the current crop.
796             // This is so we find the largest height possible which also respects the device aspect
797             // ratio.
798             int heightToAdd = (int) (0.5f + crop.width() / screenRatio - crop.height());
799 
800             // Calculate how much extra image space available that can be used to adjust
801             // the crop. If this amount is less than heightToAdd, from above, then that means we
802             // can't use heightToAdd. Instead we will need to use the maximum possible height, which
803             // is the height of the original bitmap. NOTE: the bitmap height may be different than
804             // the crop.
805             // since there is no guarantee to have height available on both sides
806             // (e.g. the available height might be fully at the bottom), grab the minimum
807             int availableHeight = 2 * Math.min(crop.top, bitmapSize.y - crop.bottom);
808             int actualHeightToAdd = Math.min(heightToAdd, availableHeight);
809 
810             // half of the additional height is added to the top and bottom of the crop
811             adjustedCrop.top -= actualHeightToAdd / 2 + actualHeightToAdd % 2;
812             adjustedCrop.bottom += actualHeightToAdd / 2;
813 
814             // Calculate the width of the adjusted crop. Initially we used the fixed width of the
815             // crop to calculate the heightToAdd, but since this height may be invalid (based on
816             // the calculation above) we calculate the width again instead of using the fixed width,
817             // using the adjustedCrop's updated height.
818             int widthToRemove = (int) (0.5f + crop.width() - adjustedCrop.height() * screenRatio);
819 
820             // half of the additional width is subtracted from the left and right side of the crop
821             int widthToRemoveLeft = widthToRemove / 2;
822             int widthToRemoveRight = widthToRemove / 2 + widthToRemove % 2;
823 
824             adjustedCrop.left += widthToRemoveLeft;
825             adjustedCrop.right -= widthToRemoveRight;
826 
827             if (DEBUG) {
828                 Slog.d(TAG, "cropRatio: " + cropRatio);
829                 Slog.d(TAG, "screenRatio: " + screenRatio);
830                 Slog.d(TAG, "heightToAdd: " + heightToAdd);
831                 Slog.d(TAG, "actualHeightToAdd: " + actualHeightToAdd);
832                 Slog.d(TAG, "availableHeight: " + availableHeight);
833                 Slog.d(TAG, "widthToRemove: " + widthToRemove);
834                 Slog.d(TAG, "adjustedCrop: " + adjustedCrop);
835             }
836 
837             return adjustedCrop;
838         }
839 
840         return adjustedCrop;
841     }
842 
isTargetMoreNarrowThanSource(Point targetDisplaySize, Point srcDisplaySize)843     private boolean isTargetMoreNarrowThanSource(Point targetDisplaySize, Point srcDisplaySize) {
844         float targetScreenRatio = (float) targetDisplaySize.x / targetDisplaySize.y;
845         float srcScreenRatio = (float) srcDisplaySize.x / srcDisplaySize.y;
846 
847         return (targetScreenRatio < srcScreenRatio);
848     }
849 
logRestoreErrorIfNoLiveComponent(int which, String error)850     private void logRestoreErrorIfNoLiveComponent(int which, String error) {
851         if (mSystemHasLiveComponent) {
852             return;
853         }
854         logRestoreError(which, error);
855     }
856 
logRestoreError(int which, String error)857     private void logRestoreError(int which, String error) {
858         if ((which & FLAG_SYSTEM) == FLAG_SYSTEM) {
859             mEventLogger.onSystemImageWallpaperRestoreFailed(error);
860         }
861         if ((which & FLAG_LOCK) == FLAG_LOCK) {
862             mEventLogger.onLockImageWallpaperRestoreFailed(error);
863         }
864     }
865 
parseCropHint(File wallpaperInfo, String sectionTag)866     private Rect parseCropHint(File wallpaperInfo, String sectionTag) {
867         Rect cropHint = new Rect();
868         try (FileInputStream stream = new FileInputStream(wallpaperInfo)) {
869             XmlPullParser parser = Xml.resolvePullParser(stream);
870 
871             int type;
872             do {
873                 type = parser.next();
874                 if (type == XmlPullParser.START_TAG) {
875                     String tag = parser.getName();
876                     if (sectionTag.equals(tag)) {
877                         cropHint.left = getAttributeInt(parser, "cropLeft", 0);
878                         cropHint.top = getAttributeInt(parser, "cropTop", 0);
879                         cropHint.right = getAttributeInt(parser, "cropRight", 0);
880                         cropHint.bottom = getAttributeInt(parser, "cropBottom", 0);
881                     }
882                 }
883             } while (type != XmlPullParser.END_DOCUMENT);
884         } catch (Exception e) {
885             // Whoops; can't process the info file at all.  Report failure.
886             Slog.w(TAG, "Failed to parse restored crop: " + e.getMessage());
887             return null;
888         }
889 
890         return cropHint;
891     }
892 
parseCropHints(File wallpaperInfo, String sectionTag)893     private SparseArray<Rect> parseCropHints(File wallpaperInfo, String sectionTag) {
894         SparseArray<Rect> cropHints = new SparseArray<>();
895         try (FileInputStream stream = new FileInputStream(wallpaperInfo)) {
896             XmlPullParser parser = Xml.resolvePullParser(stream);
897             int type;
898             do {
899                 type = parser.next();
900                 if (type != XmlPullParser.START_TAG) continue;
901                 String tag = parser.getName();
902                 if (!sectionTag.equals(tag)) continue;
903                 for (Pair<Integer, String> pair : List.of(
904                         new Pair<>(WallpaperManager.PORTRAIT, "Portrait"),
905                         new Pair<>(WallpaperManager.LANDSCAPE, "Landscape"),
906                         new Pair<>(WallpaperManager.SQUARE_PORTRAIT, "SquarePortrait"),
907                         new Pair<>(WallpaperManager.SQUARE_LANDSCAPE, "SquareLandscape"))) {
908                     Rect cropHint = new Rect(
909                             getAttributeInt(parser, "cropLeft" + pair.second, 0),
910                             getAttributeInt(parser, "cropTop" + pair.second, 0),
911                             getAttributeInt(parser, "cropRight" + pair.second, 0),
912                             getAttributeInt(parser, "cropBottom" + pair.second, 0));
913                     if (!cropHint.isEmpty()) cropHints.put(pair.first, cropHint);
914                 }
915                 if (cropHints.size() == 0) {
916                     // migration case: the crops per screen orientation are not specified.
917                     // use the old attributes to restore the crop for one screen orientation.
918                     Rect cropHint = new Rect(
919                             getAttributeInt(parser, "cropLeft", 0),
920                             getAttributeInt(parser, "cropTop", 0),
921                             getAttributeInt(parser, "cropRight", 0),
922                             getAttributeInt(parser, "cropBottom", 0));
923                     if (!cropHint.isEmpty()) cropHints.put(ORIENTATION_UNKNOWN, cropHint);
924                 }
925             } while (type != XmlPullParser.END_DOCUMENT);
926         } catch (Exception e) {
927             // Whoops; can't process the info file at all.  Report failure.
928             Slog.w(TAG, "Failed to parse restored crops: " + e.getMessage());
929             return null;
930         }
931         return cropHints;
932     }
933 
parseWallpaperComponent(File wallpaperInfo, String sectionTag)934     private ComponentName parseWallpaperComponent(File wallpaperInfo, String sectionTag) {
935         ComponentName name = null;
936         try (FileInputStream stream = new FileInputStream(wallpaperInfo)) {
937             final XmlPullParser parser = Xml.resolvePullParser(stream);
938 
939             int type;
940             do {
941                 type = parser.next();
942                 if (type == XmlPullParser.START_TAG) {
943                     String tag = parser.getName();
944                     if (sectionTag.equals(tag)) {
945                         final String parsedName = parser.getAttributeValue(null, "component");
946                         name = (parsedName != null)
947                                 ? ComponentName.unflattenFromString(parsedName)
948                                 : null;
949                         break;
950                     }
951                 }
952             } while (type != XmlPullParser.END_DOCUMENT);
953         } catch (Exception e) {
954             // Whoops; can't process the info file at all.  Report failure.
955             Slog.w(TAG, "Failed to parse restored component: " + e.getMessage());
956             return null;
957         }
958         return name;
959     }
960 
getAttributeInt(XmlPullParser parser, String name, int defValue)961     private int getAttributeInt(XmlPullParser parser, String name, int defValue) {
962         final String value = parser.getAttributeValue(null, name);
963         return (value == null) ? defValue : Integer.parseInt(value);
964     }
965 
966     @VisibleForTesting
servicePackageExists(ComponentName comp)967     boolean servicePackageExists(ComponentName comp) {
968         try {
969             if (comp != null) {
970                 final IPackageManager pm = AppGlobals.getPackageManager();
971                 final PackageInfo info = pm.getPackageInfo(comp.getPackageName(),
972                         0, getUserId());
973                 return (info != null);
974             }
975         } catch (RemoteException e) {
976             Slog.e(TAG, "Unable to contact package manager");
977         }
978         return false;
979     }
980 
981     /** Unused Key/Value API. */
982     @Override
onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState)983     public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
984             ParcelFileDescriptor newState) throws IOException {
985         // Intentionally blank
986     }
987 
988     /** Unused Key/Value API. */
989     @Override
onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)990     public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
991             throws IOException {
992         // Intentionally blank
993     }
994 
applyComponentAtInstall(ComponentName componentName, int which)995     private void applyComponentAtInstall(ComponentName componentName, int which) {
996         PackageMonitor packageMonitor = getWallpaperPackageMonitor(
997                 componentName, which);
998         packageMonitor.register(getBaseContext(), null, UserHandle.ALL, true);
999     }
1000 
1001     @VisibleForTesting
getWallpaperPackageMonitor(ComponentName componentName, int which)1002     PackageMonitor getWallpaperPackageMonitor(ComponentName componentName, int which) {
1003         return new PackageMonitor() {
1004             @Override
1005             public void onPackageAdded(String packageName, int uid) {
1006                 if (!isDeviceInRestore()) {
1007                     // We don't want to reapply the wallpaper outside a restore.
1008                     unregister();
1009 
1010                     // We have finished restore and not succeeded, so let's log that as an error.
1011                     WallpaperEventLogger logger = new WallpaperEventLogger(
1012                             mBackupManager.getDelayedRestoreLogger());
1013                     if ((which & FLAG_SYSTEM) != 0) {
1014                         logger.onSystemLiveWallpaperRestoreFailed(
1015                                 WallpaperEventLogger.ERROR_LIVE_PACKAGE_NOT_INSTALLED);
1016                     }
1017                     if ((which & FLAG_LOCK) != 0) {
1018                         logger.onLockLiveWallpaperRestoreFailed(
1019                                 WallpaperEventLogger.ERROR_LIVE_PACKAGE_NOT_INSTALLED);
1020                     }
1021                     mBackupManager.reportDelayedRestoreResult(logger.getBackupRestoreLogger());
1022 
1023                     return;
1024                 }
1025 
1026                 if (componentName.getPackageName().equals(packageName)) {
1027                     Slog.d(TAG, "Applying component " + componentName);
1028                     boolean success = mWallpaperManager.setWallpaperComponentWithFlags(
1029                             componentName, which);
1030                     WallpaperEventLogger logger = new WallpaperEventLogger(
1031                             mBackupManager.getDelayedRestoreLogger());
1032                     if (success) {
1033                         if ((which & FLAG_SYSTEM) != 0) {
1034                             logger.onSystemLiveWallpaperRestored(componentName);
1035                         }
1036                         if ((which & FLAG_LOCK) != 0) {
1037                             logger.onLockLiveWallpaperRestored(componentName);
1038                         }
1039                     } else {
1040                         if ((which & FLAG_SYSTEM) != 0) {
1041                             logger.onSystemLiveWallpaperRestoreFailed(
1042                                     WallpaperEventLogger.ERROR_SET_COMPONENT_EXCEPTION);
1043                         }
1044                         if ((which & FLAG_LOCK) != 0) {
1045                             logger.onLockLiveWallpaperRestoreFailed(
1046                                     WallpaperEventLogger.ERROR_SET_COMPONENT_EXCEPTION);
1047                         }
1048                     }
1049                     // We're only expecting to restore the wallpaper component once.
1050                     unregister();
1051                     mBackupManager.reportDelayedRestoreResult(logger.getBackupRestoreLogger());
1052                 }
1053             }
1054         };
1055     }
1056 
1057     /**
1058      * This method retrieves the dimensions of the largest display of the device
1059      *
1060      * @return a @{Point} object that contains the dimensions of the largest display on the device
1061      */
1062     private Point getScreenDimensions() {
1063         Point largetDimensions = null;
1064         int maxArea = 0;
1065 
1066         for (Display display : getInternalDisplays()) {
1067             Point displaySize = getRealSize(display);
1068 
1069             int width = displaySize.x;
1070             int height = displaySize.y;
1071             int area = width * height;
1072 
1073             if (area > maxArea) {
1074                 maxArea = area;
1075                 largetDimensions = displaySize;
1076             }
1077         }
1078 
1079         return largetDimensions;
1080     }
1081 
1082     private Point getRealSize(Display display) {
1083         DisplayInfo displayInfo = new DisplayInfo();
1084         display.getDisplayInfo(displayInfo);
1085         return new Point(displayInfo.logicalWidth, displayInfo.logicalHeight);
1086     }
1087 
1088     /**
1089      * This method returns the smaller display on a multi-display device
1090      *
1091      * @return Display that corresponds to the smaller display on a device or null if ther is only
1092      * one Display on a device
1093      */
1094     private Display getSmallerDisplayIfExists() {
1095         List<Display> internalDisplays = getInternalDisplays();
1096         Point largestDisplaySize = getScreenDimensions();
1097 
1098         // Find the first non-matching internal display
1099         for (Display display : internalDisplays) {
1100             Point displaySize = getRealSize(display);
1101             if (displaySize.x != largestDisplaySize.x || displaySize.y != largestDisplaySize.y) {
1102                 return display;
1103             }
1104         }
1105 
1106         // If no smaller display found, return null, as there is only a single display
1107         return null;
1108     }
1109 
1110     /**
1111      * This method retrieves the collection of Display objects available in the device.
1112      * i.e. non-external displays are ignored
1113      *
1114      * @return list of displays corresponding to each display in the device
1115      */
1116     private List<Display> getInternalDisplays() {
1117         Display[] allDisplays = mDisplayManager.getDisplays(
1118                 DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED);
1119 
1120         List<Display> internalDisplays = new ArrayList<>();
1121         for (Display display : allDisplays) {
1122             if (display.getType() == Display.TYPE_INTERNAL) {
1123                 internalDisplays.add(display);
1124             }
1125         }
1126         return internalDisplays;
1127     }
1128 
1129     @VisibleForTesting
1130     boolean isDeviceInRestore() {
1131         try {
1132             boolean isInSetup = Settings.Secure.getInt(getBaseContext().getContentResolver(),
1133                     Settings.Secure.USER_SETUP_COMPLETE) == 0;
1134             boolean isInDeferredSetup = Settings.Secure.getInt(getBaseContext()
1135                             .getContentResolver(),
1136                     Settings.Secure.USER_SETUP_PERSONALIZATION_STATE) ==
1137                     Settings.Secure.USER_SETUP_PERSONALIZATION_STARTED;
1138             return isInSetup || isInDeferredSetup;
1139         } catch (Settings.SettingNotFoundException e) {
1140             Slog.w(TAG, "Failed to check if the user is in restore: " + e);
1141             return false;
1142         }
1143     }
1144 
1145     @VisibleForTesting
1146     void setBackupManagerForTesting(BackupManager backupManager) {
1147         mBackupManager = backupManager;
1148     }
1149 }