Home | History | Annotate | Download | only in wallpaperbackup
      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 
     22 import android.app.AppGlobals;
     23 import android.app.WallpaperManager;
     24 import android.app.backup.BackupAgent;
     25 import android.app.backup.BackupDataInput;
     26 import android.app.backup.BackupDataOutput;
     27 import android.app.backup.FullBackupDataOutput;
     28 import android.content.ComponentName;
     29 import android.content.Context;
     30 import android.content.SharedPreferences;
     31 import android.content.pm.IPackageManager;
     32 import android.content.pm.PackageInfo;
     33 import android.graphics.Rect;
     34 import android.os.Environment;
     35 import android.os.FileUtils;
     36 import android.os.ParcelFileDescriptor;
     37 import android.os.RemoteException;
     38 import android.os.UserHandle;
     39 import android.util.Slog;
     40 import android.util.Xml;
     41 
     42 import libcore.io.IoUtils;
     43 
     44 import org.xmlpull.v1.XmlPullParser;
     45 
     46 import java.io.File;
     47 import java.io.FileInputStream;
     48 import java.io.FileOutputStream;
     49 import java.io.IOException;
     50 import java.nio.charset.StandardCharsets;
     51 
     52 public class WallpaperBackupAgent extends BackupAgent {
     53     private static final String TAG = "WallpaperBackup";
     54     private static final boolean DEBUG = false;
     55 
     56     // NB: must be kept in sync with WallpaperManagerService but has no
     57     // compile-time visibility.
     58 
     59     // Target filenames within the system's wallpaper directory
     60     static final String WALLPAPER = "wallpaper_orig";
     61     static final String WALLPAPER_LOCK = "wallpaper_lock_orig";
     62     static final String WALLPAPER_INFO = "wallpaper_info.xml";
     63 
     64     // Names of our local-data stage files/links
     65     static final String IMAGE_STAGE = "wallpaper-stage";
     66     static final String LOCK_IMAGE_STAGE = "wallpaper-lock-stage";
     67     static final String INFO_STAGE = "wallpaper-info-stage";
     68     static final String EMPTY_SENTINEL = "empty";
     69     static final String QUOTA_SENTINEL = "quota";
     70 
     71     // Not-for-backup bookkeeping
     72     static final String PREFS_NAME = "wbprefs.xml";
     73     static final String SYSTEM_GENERATION = "system_gen";
     74     static final String LOCK_GENERATION = "lock_gen";
     75 
     76     private File mWallpaperInfo;        // wallpaper metadata file
     77     private File mWallpaperFile;        // primary wallpaper image file
     78     private File mLockWallpaperFile;    // lock wallpaper image file
     79 
     80     // If this file exists, it means we exceeded our quota last time
     81     private File mQuotaFile;
     82     private boolean mQuotaExceeded;
     83 
     84     private WallpaperManager mWm;
     85 
     86     @Override
     87     public void onCreate() {
     88         if (DEBUG) {
     89             Slog.v(TAG, "onCreate()");
     90         }
     91 
     92         File wallpaperDir = Environment.getUserSystemDirectory(UserHandle.USER_SYSTEM);
     93         mWallpaperInfo = new File(wallpaperDir, WALLPAPER_INFO);
     94         mWallpaperFile = new File(wallpaperDir, WALLPAPER);
     95         mLockWallpaperFile = new File(wallpaperDir, WALLPAPER_LOCK);
     96         mWm = (WallpaperManager) getSystemService(Context.WALLPAPER_SERVICE);
     97 
     98         mQuotaFile = new File(getFilesDir(), QUOTA_SENTINEL);
     99         mQuotaExceeded = mQuotaFile.exists();
    100         if (DEBUG) {
    101             Slog.v(TAG, "quota file " + mQuotaFile.getPath() + " exists=" + mQuotaExceeded);
    102         }
    103     }
    104 
    105     @Override
    106     public void onFullBackup(FullBackupDataOutput data) throws IOException {
    107         // To avoid data duplication and disk churn, use links as the stage.
    108         final File filesDir = getFilesDir();
    109         final File infoStage = new File(filesDir, INFO_STAGE);
    110         final File imageStage = new File (filesDir, IMAGE_STAGE);
    111         final File lockImageStage = new File (filesDir, LOCK_IMAGE_STAGE);
    112         final File empty = new File (filesDir, EMPTY_SENTINEL);
    113 
    114         try {
    115             // We always back up this 'empty' file to ensure that the absence of
    116             // storable wallpaper imagery still produces a non-empty backup data
    117             // stream, otherwise it'd simply be ignored in preflight.
    118             FileOutputStream touch = new FileOutputStream(empty);
    119             touch.close();
    120             fullBackupFile(empty, data);
    121 
    122             SharedPreferences prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
    123             final int lastSysGeneration = prefs.getInt(SYSTEM_GENERATION, -1);
    124             final int lastLockGeneration = prefs.getInt(LOCK_GENERATION, -1);
    125 
    126             final int sysGeneration =
    127                     mWm.getWallpaperIdForUser(FLAG_SYSTEM, UserHandle.USER_SYSTEM);
    128             final int lockGeneration =
    129                     mWm.getWallpaperIdForUser(FLAG_LOCK, UserHandle.USER_SYSTEM);
    130             final boolean sysChanged = (sysGeneration != lastSysGeneration);
    131             final boolean lockChanged = (lockGeneration != lastLockGeneration);
    132 
    133             final boolean sysEligible = mWm.isWallpaperBackupEligible(FLAG_SYSTEM);
    134             final boolean lockEligible = mWm.isWallpaperBackupEligible(FLAG_LOCK);
    135 
    136                 // There might be a latent lock wallpaper file present but unused: don't
    137                 // include it in the backup if that's the case.
    138                 ParcelFileDescriptor lockFd = mWm.getWallpaperFile(FLAG_LOCK, UserHandle.USER_SYSTEM);
    139                 final boolean hasLockWallpaper = (lockFd != null);
    140                 IoUtils.closeQuietly(lockFd);
    141 
    142             if (DEBUG) {
    143                 Slog.v(TAG, "sysGen=" + sysGeneration + " : sysChanged=" + sysChanged);
    144                 Slog.v(TAG, "lockGen=" + lockGeneration + " : lockChanged=" + lockChanged);
    145                 Slog.v(TAG, "sysEligble=" + sysEligible);
    146                 Slog.v(TAG, "lockEligible=" + lockEligible);
    147             }
    148 
    149             // only back up the wallpapers if we've been told they're eligible
    150             if (mWallpaperInfo.exists()) {
    151                 if (sysChanged || lockChanged || !infoStage.exists()) {
    152                     if (DEBUG) Slog.v(TAG, "New wallpaper configuration; copying");
    153                     FileUtils.copyFileOrThrow(mWallpaperInfo, infoStage);
    154                 }
    155                 if (DEBUG) Slog.v(TAG, "Storing wallpaper metadata");
    156                 fullBackupFile(infoStage, data);
    157             }
    158             if (sysEligible && mWallpaperFile.exists()) {
    159                 if (sysChanged || !imageStage.exists()) {
    160                     if (DEBUG) Slog.v(TAG, "New system wallpaper; copying");
    161                     FileUtils.copyFileOrThrow(mWallpaperFile, imageStage);
    162                 }
    163                 if (DEBUG) Slog.v(TAG, "Storing system wallpaper image");
    164                 fullBackupFile(imageStage, data);
    165                 prefs.edit().putInt(SYSTEM_GENERATION, sysGeneration).apply();
    166             }
    167 
    168             // Don't try to store the lock image if we overran our quota last time
    169             if (lockEligible && hasLockWallpaper && mLockWallpaperFile.exists() && !mQuotaExceeded) {
    170                 if (lockChanged || !lockImageStage.exists()) {
    171                     if (DEBUG) Slog.v(TAG, "New lock wallpaper; copying");
    172                     FileUtils.copyFileOrThrow(mLockWallpaperFile, lockImageStage);
    173                 }
    174                 if (DEBUG) Slog.v(TAG, "Storing lock wallpaper image");
    175                 fullBackupFile(lockImageStage, data);
    176                 prefs.edit().putInt(LOCK_GENERATION, lockGeneration).apply();
    177             }
    178         } catch (Exception e) {
    179             Slog.e(TAG, "Unable to back up wallpaper", e);
    180         } finally {
    181             // Even if this time we had to back off on attempting to store the lock image
    182             // due to exceeding the data quota, try again next time.  This will alternate
    183             // between "try both" and "only store the primary image" until either there
    184             // is no lock image to store, or the quota is raised, or both fit under the
    185             // quota.
    186             mQuotaFile.delete();
    187         }
    188     }
    189 
    190     @Override
    191     public void onQuotaExceeded(long backupDataBytes, long quotaBytes) {
    192         if (DEBUG) {
    193             Slog.i(TAG, "Quota exceeded (" + backupDataBytes + " vs " + quotaBytes + ')');
    194         }
    195         try (FileOutputStream f = new FileOutputStream(mQuotaFile)) {
    196             f.write(0);
    197         } catch (Exception e) {
    198             Slog.w(TAG, "Unable to record quota-exceeded: " + e.getMessage());
    199         }
    200     }
    201 
    202     // We use the default onRestoreFile() implementation that will recreate our stage files,
    203     // then post-process in onRestoreFinished() to apply the new wallpaper.
    204     @Override
    205     public void onRestoreFinished() {
    206         if (DEBUG) {
    207             Slog.v(TAG, "onRestoreFinished()");
    208         }
    209         final File filesDir = getFilesDir();
    210         final File infoStage = new File(filesDir, INFO_STAGE);
    211         final File imageStage = new File (filesDir, IMAGE_STAGE);
    212         final File lockImageStage = new File (filesDir, LOCK_IMAGE_STAGE);
    213 
    214         // If we restored separate lock imagery, the system wallpaper should be
    215         // applied as system-only; but if there's no separate lock image, make
    216         // sure to apply the restored system wallpaper as both.
    217         final int sysWhich = FLAG_SYSTEM | (lockImageStage.exists() ? 0 : FLAG_LOCK);
    218 
    219         try {
    220             // It is valid for the imagery to be absent; it means that we were not permitted
    221             // to back up the original image on the source device, or there was no user-supplied
    222             // wallpaper image present.
    223             restoreFromStage(imageStage, infoStage, "wp", sysWhich);
    224             restoreFromStage(lockImageStage, infoStage, "kwp", FLAG_LOCK);
    225 
    226             // And reset to the wallpaper service we should be using
    227             ComponentName wpService = parseWallpaperComponent(infoStage, "wp");
    228             if (servicePackageExists(wpService)) {
    229                 if (DEBUG) {
    230                     Slog.i(TAG, "Using wallpaper service " + wpService);
    231                 }
    232                 mWm.setWallpaperComponent(wpService, UserHandle.USER_SYSTEM);
    233                 if (!lockImageStage.exists()) {
    234                     // We have a live wallpaper and no static lock image,
    235                     // allow live wallpaper to show "through" on lock screen.
    236                     mWm.clear(FLAG_LOCK);
    237                 }
    238             } else {
    239                 if (DEBUG) {
    240                     Slog.v(TAG, "Can't use wallpaper service " + wpService);
    241                 }
    242             }
    243         } catch (Exception e) {
    244             Slog.e(TAG, "Unable to restore wallpaper: " + e.getMessage());
    245         } finally {
    246             if (DEBUG) {
    247                 Slog.v(TAG, "Restore finished; clearing backup bookkeeping");
    248             }
    249             infoStage.delete();
    250             imageStage.delete();
    251             lockImageStage.delete();
    252 
    253             SharedPreferences prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
    254             prefs.edit()
    255                     .putInt(SYSTEM_GENERATION, -1)
    256                     .putInt(LOCK_GENERATION, -1)
    257                     .commit();
    258         }
    259     }
    260 
    261     private void restoreFromStage(File stage, File info, String hintTag, int which)
    262             throws IOException {
    263         if (stage.exists()) {
    264             // Parse the restored info file to find the crop hint.  Note that this currently
    265             // relies on a priori knowledge of the wallpaper info file schema.
    266             Rect cropHint = parseCropHint(info, hintTag);
    267             if (cropHint != null) {
    268                 Slog.i(TAG, "Got restored wallpaper; applying which=" + which);
    269                 if (DEBUG) {
    270                     Slog.v(TAG, "Restored crop hint " + cropHint);
    271                 }
    272                 try (FileInputStream in = new FileInputStream(stage)) {
    273                     mWm.setStream(in, cropHint.isEmpty() ? null : cropHint, true, which);
    274                 } finally {} // auto-closes 'in'
    275             }
    276         }
    277     }
    278 
    279     private Rect parseCropHint(File wallpaperInfo, String sectionTag) {
    280         Rect cropHint = new Rect();
    281         try (FileInputStream stream = new FileInputStream(wallpaperInfo)) {
    282             XmlPullParser parser = Xml.newPullParser();
    283             parser.setInput(stream, StandardCharsets.UTF_8.name());
    284 
    285             int type;
    286             do {
    287                 type = parser.next();
    288                 if (type == XmlPullParser.START_TAG) {
    289                     String tag = parser.getName();
    290                     if (sectionTag.equals(tag)) {
    291                         cropHint.left = getAttributeInt(parser, "cropLeft", 0);
    292                         cropHint.top = getAttributeInt(parser, "cropTop", 0);
    293                         cropHint.right = getAttributeInt(parser, "cropRight", 0);
    294                         cropHint.bottom = getAttributeInt(parser, "cropBottom", 0);
    295                     }
    296                 }
    297             } while (type != XmlPullParser.END_DOCUMENT);
    298         } catch (Exception e) {
    299             // Whoops; can't process the info file at all.  Report failure.
    300             Slog.w(TAG, "Failed to parse restored crop: " + e.getMessage());
    301             return null;
    302         }
    303 
    304         return cropHint;
    305     }
    306 
    307     private ComponentName parseWallpaperComponent(File wallpaperInfo, String sectionTag) {
    308         ComponentName name = null;
    309         try (FileInputStream stream = new FileInputStream(wallpaperInfo)) {
    310             final XmlPullParser parser = Xml.newPullParser();
    311             parser.setInput(stream, StandardCharsets.UTF_8.name());
    312 
    313             int type;
    314             do {
    315                 type = parser.next();
    316                 if (type == XmlPullParser.START_TAG) {
    317                     String tag = parser.getName();
    318                     if (sectionTag.equals(tag)) {
    319                         final String parsedName = parser.getAttributeValue(null, "component");
    320                         name = (parsedName != null)
    321                                 ? ComponentName.unflattenFromString(parsedName)
    322                                 : null;
    323                         break;
    324                     }
    325                 }
    326             } while (type != XmlPullParser.END_DOCUMENT);
    327         } catch (Exception e) {
    328             // Whoops; can't process the info file at all.  Report failure.
    329             Slog.w(TAG, "Failed to parse restored component: " + e.getMessage());
    330             return null;
    331         }
    332         return name;
    333     }
    334 
    335     private int getAttributeInt(XmlPullParser parser, String name, int defValue) {
    336         final String value = parser.getAttributeValue(null, name);
    337         return (value == null) ? defValue : Integer.parseInt(value);
    338     }
    339 
    340     private boolean servicePackageExists(ComponentName comp) {
    341         try {
    342             if (comp != null) {
    343                 final IPackageManager pm = AppGlobals.getPackageManager();
    344                 final PackageInfo info = pm.getPackageInfo(comp.getPackageName(),
    345                         0, UserHandle.USER_SYSTEM);
    346                 return (info != null);
    347             }
    348         } catch (RemoteException e) {
    349             Slog.e(TAG, "Unable to contact package manager");
    350         }
    351         return false;
    352     }
    353 
    354     //
    355     // Key/value API: abstract, therefore required; but not used
    356     //
    357 
    358     @Override
    359     public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
    360             ParcelFileDescriptor newState) throws IOException {
    361         // Intentionally blank
    362     }
    363 
    364     @Override
    365     public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
    366             throws IOException {
    367         // Intentionally blank
    368     }
    369 }