Merge "Extract crop hint rect from source wallpaper image"
diff --git a/core/java/android/app/IWallpaperManager.aidl b/core/java/android/app/IWallpaperManager.aidl
index 7a0e7f6..fa0fbd1 100644
--- a/core/java/android/app/IWallpaperManager.aidl
+++ b/core/java/android/app/IWallpaperManager.aidl
@@ -35,10 +35,16 @@
      * 'which' is some combination of:
      *   FLAG_SET_SYSTEM
      *   FLAG_SET_LOCK
+     *
+     * A 'null' cropHint rectangle is explicitly permitted as a sentinel for "whatever
+     * the source image's bounding rect is."
+     *
+     * The completion callback's "onWallpaperChanged()" method is invoked when the
+     * new wallpaper content is ready to display.
      */
     ParcelFileDescriptor setWallpaper(String name, in String callingPackage,
-            out Bundle extras, int which);
-    
+            in Rect cropHint, out Bundle extras, int which, IWallpaperManagerCallback completion);
+
     /**
      * Set the live wallpaper. This only affects the system wallpaper.
      */
@@ -54,14 +60,14 @@
      */
     ParcelFileDescriptor getWallpaper(IWallpaperManagerCallback cb,
             out Bundle outParams);
-    
+
     /**
      * If the current system wallpaper is a live wallpaper component, return the
      * information about that wallpaper.  Otherwise, if it is a static image,
      * simply return null.
      */
     WallpaperInfo getWallpaperInfo();
-    
+
     /**
      * Clear the system wallpaper.
      */
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index f103576..b0ffd21 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -64,6 +64,8 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Provides access to the system wallpaper. With WallpaperManager, you can
@@ -770,18 +772,26 @@
             return 0;
         }
         final Bundle result = new Bundle();
+        final WallpaperSetCompletion completion = new WallpaperSetCompletion();
         try {
             Resources resources = mContext.getResources();
             /* Set the wallpaper to the default values */
             ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(
                     "res:" + resources.getResourceName(resid),
-                    mContext.getOpPackageName(), result, which);
+                    mContext.getOpPackageName(), null, result, which, completion);
             if (fd != null) {
                 FileOutputStream fos = null;
+                boolean ok = false;
                 try {
                     fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd);
                     copyStreamToWallpaperFile(resources.openRawResource(resid), fos);
+                    // The 'close()' is the trigger for any server-side image manipulation,
+                    // so we must do that before waiting for completion.
+                    fos.close();
+                    completion.waitForCompletion();
                 } finally {
+                    // Might be redundant but completion shouldn't wait unless the write
+                    // succeeded; this is a fallback if it threw past the close+wait.
                     IoUtils.closeQuietly(fos);
                 }
             }
@@ -876,14 +886,17 @@
             return 0;
         }
         final Bundle result = new Bundle();
+        final WallpaperSetCompletion completion = new WallpaperSetCompletion();
         try {
             ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(null,
-                    mContext.getOpPackageName(), result, which);
+                    mContext.getOpPackageName(), visibleCropHint, result, which, completion);
             if (fd != null) {
                 FileOutputStream fos = null;
                 try {
                     fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd);
                     fullImage.compress(Bitmap.CompressFormat.PNG, 90, fos);
+                    fos.close();
+                    completion.waitForCompletion();
                 } finally {
                     IoUtils.closeQuietly(fos);
                 }
@@ -990,14 +1003,17 @@
             return 0;
         }
         final Bundle result = new Bundle();
+        final WallpaperSetCompletion completion = new WallpaperSetCompletion();
         try {
             ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(null,
-                    mContext.getOpPackageName(), result, which);
+                    mContext.getOpPackageName(), visibleCropHint, result, which, completion);
             if (fd != null) {
                 FileOutputStream fos = null;
                 try {
                     fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd);
                     copyStreamToWallpaperFile(bitmapData, fos);
+                    fos.close();
+                    completion.waitForCompletion();
                 } finally {
                     IoUtils.closeQuietly(fos);
                 }
@@ -1385,4 +1401,28 @@
 
         return null;
     }
+
+    // Private completion callback for setWallpaper() synchronization
+    private class WallpaperSetCompletion extends IWallpaperManagerCallback.Stub {
+        final CountDownLatch mLatch;
+
+        public WallpaperSetCompletion() {
+            mLatch = new CountDownLatch(1);
+        }
+
+        public void waitForCompletion() {
+            try {
+                mLatch.await(30, TimeUnit.SECONDS);
+            } catch (InterruptedException e) {
+                // This might be legit: the crop may take a very long time. Don't sweat
+                // it in that case; we are okay with display lagging behind in order to
+                // keep the caller from locking up indeterminately.
+            }
+        }
+
+        @Override
+        public void onWallpaperChanged() throws RemoteException {
+            mLatch.countDown();
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 39983dd..c7d7096 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -42,6 +42,9 @@
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.UserInfo;
 import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.Binder;
@@ -71,6 +74,7 @@
 import android.view.IWindowManager;
 import android.view.WindowManager;
 
+import java.io.BufferedOutputStream;
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.io.InputStream;
@@ -96,7 +100,7 @@
 
 public class WallpaperManagerService extends IWallpaperManager.Stub {
     static final String TAG = "WallpaperManagerService";
-    static final boolean DEBUG = false;
+    static final boolean DEBUG = true;
 
     final Object mLock = new Object[0];
 
@@ -106,7 +110,8 @@
      */
     static final long MIN_WALLPAPER_CRASH_TIME = 10000;
     static final int MAX_WALLPAPER_COMPONENT_LOG_LENGTH = 128;
-    static final String WALLPAPER = "wallpaper";
+    static final String WALLPAPER = "wallpaper_orig";
+    static final String WALLPAPER_CROP = "wallpaper";
     static final String WALLPAPER_INFO = "wallpaper_info.xml";
 
     /**
@@ -120,6 +125,7 @@
         final WallpaperData mWallpaper;
         final File mWallpaperDir;
         final File mWallpaperFile;
+        final File mWallpaperCropFile;
         final File mWallpaperInfoFile;
 
         public WallpaperObserver(WallpaperData wallpaper) {
@@ -128,6 +134,7 @@
             mWallpaperDir = getWallpaperDir(wallpaper.userId);
             mWallpaper = wallpaper;
             mWallpaperFile = new File(mWallpaperDir, WALLPAPER);
+            mWallpaperCropFile = new File(mWallpaperDir, WALLPAPER_CROP);
             mWallpaperInfoFile = new File(mWallpaperDir, WALLPAPER_INFO);
         }
 
@@ -136,8 +143,10 @@
             if (path == null) {
                 return;
             }
+            final boolean written = (event == CLOSE_WRITE || event == MOVED_TO);
+            final File changedFile = new File(mWallpaperDir, path);
+
             synchronized (mLock) {
-                File changedFile = new File(mWallpaperDir, path);
                 if (mWallpaperFile.equals(changedFile)
                         || mWallpaperInfoFile.equals(changedFile)) {
                     // changing the wallpaper means we'll need to back up the new one
@@ -148,22 +157,111 @@
                 }
                 if (mWallpaperFile.equals(changedFile)) {
                     notifyCallbacksLocked(mWallpaper);
-                    final boolean written = (event == CLOSE_WRITE || event == MOVED_TO);
                     if (mWallpaper.wallpaperComponent == null
                             || event != CLOSE_WRITE // includes the MOVED_TO case
                             || mWallpaper.imageWallpaperPending) {
                         if (written) {
+                            // The image source has finished writing the source image,
+                            // so we now produce the crop rect (in the background), and
+                            // only publish the new displayable (sub)image as a result
+                            // of that work.
+                            generateCrop(mWallpaper);
                             mWallpaper.imageWallpaperPending = false;
+                            if (mWallpaper.setComplete != null) {
+                                try {
+                                    mWallpaper.setComplete.onWallpaperChanged();
+                                } catch (RemoteException e) {
+                                    // if this fails we don't really care; the setting app may just
+                                    // have crashed and that sort of thing is a fact of life.
+                                }
+                            }
+                            bindWallpaperComponentLocked(mImageWallpaper, true,
+                                    false, mWallpaper, null);
+                            saveSettingsLocked(mWallpaper);
                         }
-                        bindWallpaperComponentLocked(mImageWallpaper, true,
-                                false, mWallpaper, null);
-                        saveSettingsLocked(mWallpaper);
                     }
                 }
             }
         }
     }
 
+    /**
+     * Once a new wallpaper has been written via setWallpaper(...), it needs to be cropped
+     * for display.
+     */
+    private void generateCrop(WallpaperData wallpaper) {
+        boolean success = false;
+        boolean needCrop = false;
+
+        // Analyse the source; needed in multiple cases
+        BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inJustDecodeBounds = true;
+        BitmapFactory.decodeFile(wallpaper.wallpaperFile.getAbsolutePath(), options);
+
+        // Legacy case uses an empty crop rect here, so we just preserve the
+        // source image verbatim
+        if (!wallpaper.cropHint.isEmpty()) {
+            // ...clamp the crop rect to the measured bounds...
+            wallpaper.cropHint.right = Math.min(wallpaper.cropHint.right, options.outWidth);
+            wallpaper.cropHint.bottom = Math.min(wallpaper.cropHint.bottom, options.outHeight);
+            // ...and don't bother cropping if what we're left with is identity
+            needCrop = (options.outHeight >= wallpaper.cropHint.height()
+                    && options.outWidth >= wallpaper.cropHint.width());
+        }
+
+        if (!needCrop) {
+            // Simple case:  the nominal crop is at least as big as the source image,
+            // so we take the whole thing and just copy the image file directly.
+            if (DEBUG) {
+                Slog.v(TAG, "Null crop of new wallpaper; copying");
+            }
+            success = FileUtils.copyFile(wallpaper.wallpaperFile, wallpaper.cropFile);
+            if (!success) {
+                wallpaper.cropFile.delete();
+                // TODO: fall back to default wallpaper in this case
+            }
+        } else {
+            // Fancy case: the crop is a subrect of the source
+            FileOutputStream f = null;
+            BufferedOutputStream bos = null;
+            try {
+                BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(
+                        wallpaper.wallpaperFile.getAbsolutePath(), false);
+                Bitmap cropped = decoder.decodeRegion(wallpaper.cropHint, null);
+                decoder.recycle();
+
+                if (cropped == null) {
+                    Slog.e(TAG, "Could not decode new wallpaper");
+                } else {
+                    f = new FileOutputStream(wallpaper.cropFile);
+                    bos = new BufferedOutputStream(f, 32*1024);
+                    cropped.compress(Bitmap.CompressFormat.PNG, 90, bos);
+                    bos.flush();  // don't rely on the implicit flush-at-close when noting success
+                    success = true;
+                }
+            } catch (IOException e) {
+                if (DEBUG) {
+                    Slog.e(TAG, "I/O error decoding crop: " + e.getMessage());
+                }
+            } finally {
+                IoUtils.closeQuietly(bos);
+                IoUtils.closeQuietly(f);
+            }
+        }
+
+        if (!success) {
+            Slog.e(TAG, "Unable to apply new wallpaper");
+            wallpaper.cropFile.delete();
+        }
+
+        if (wallpaper.cropFile.exists()) {
+            boolean didRestorecon = SELinux.restorecon(wallpaper.cropFile.getAbsoluteFile());
+            if (DEBUG) {
+                Slog.v(TAG, "restorecon() of crop file returned " + didRestorecon);
+            }
+        }
+    }
+
     final Context mContext;
     final IWindowManager mIWindowManager;
     final IPackageManager mIPackageManager;
@@ -191,7 +289,8 @@
 
         int userId;
 
-        File wallpaperFile;
+        final File wallpaperFile;
+        final File cropFile;
 
         /**
          * Client is currently writing a new image wallpaper.
@@ -199,6 +298,11 @@
         boolean imageWallpaperPending;
 
         /**
+         * Callback once the set + crop is finished
+         */
+        IWallpaperManagerCallback setComplete;
+
+        /**
          * Resource name if using a picture from the wallpaper gallery
          */
         String name = "";
@@ -232,11 +336,26 @@
         int width = -1;
         int height = -1;
 
+        /**
+         * The crop hint supplied for displaying a subset of the source image
+         */
+        final Rect cropHint = new Rect(0, 0, 0, 0);
+
         final Rect padding = new Rect(0, 0, 0, 0);
 
         WallpaperData(int userId) {
             this.userId = userId;
             wallpaperFile = new File(getWallpaperDir(userId), WALLPAPER);
+            cropFile = new File(getWallpaperDir(userId), WALLPAPER_CROP);
+        }
+
+        // Only called in single-threaded boot sequence mode
+        boolean ensureCropExists() {
+            // if the crop file is not present, copy over the source image to use verbatim
+            if (!cropFile.exists()) {
+                return FileUtils.copyFile(wallpaperFile, cropFile);
+            }
+            return true;
         }
     }
 
@@ -524,6 +643,9 @@
     public void systemRunning() {
         if (DEBUG) Slog.v(TAG, "systemReady");
         WallpaperData wallpaper = mWallpaperMap.get(UserHandle.USER_SYSTEM);
+        if (!wallpaper.ensureCropExists()) {
+            clearWallpaperLocked(false, UserHandle.USER_SYSTEM, null);
+        }
         switchWallpaper(wallpaper, null);
         wallpaper.wallpaperObserver = new WallpaperObserver(wallpaper);
         wallpaper.wallpaperObserver.startWatching();
@@ -602,6 +724,8 @@
             onStoppingUser(userId);
             File wallpaperFile = new File(getWallpaperDir(userId), WALLPAPER);
             wallpaperFile.delete();
+            File cropFile = new File(getWallpaperDir(userId), WALLPAPER_CROP);
+            cropFile.delete();
             File wallpaperInfoFile = new File(getWallpaperDir(userId), WALLPAPER_INFO);
             wallpaperInfoFile.delete();
         }
@@ -653,9 +777,9 @@
         if (wallpaper == null) {
             return;
         }
-        File f = new File(getWallpaperDir(userId), WALLPAPER);
-        if (f.exists()) {
-            f.delete();
+        if (wallpaper.wallpaperFile.exists()) {
+            wallpaper.wallpaperFile.delete();
+            wallpaper.cropFile.delete();
         }
         final long ident = Binder.clearCallingIdentity();
         try {
@@ -844,11 +968,10 @@
                     outParams.putInt("height", wallpaper.height);
                 }
                 wallpaper.callbacks.register(cb);
-                File f = new File(getWallpaperDir(wallpaperUserId), WALLPAPER);
-                if (!f.exists()) {
+                if (!wallpaper.cropFile.exists()) {
                     return null;
                 }
-                return ParcelFileDescriptor.open(f, MODE_READ_ONLY);
+                return ParcelFileDescriptor.open(wallpaper.cropFile, MODE_READ_ONLY);
             } catch (FileNotFoundException e) {
                 /* Shouldn't happen as we check to see if the file exists */
                 Slog.w(TAG, "Error getting wallpaper", e);
@@ -869,8 +992,8 @@
     }
 
     @Override
-    public ParcelFileDescriptor setWallpaper(String name, String callingPackage, Bundle extras,
-            int which) {
+    public ParcelFileDescriptor setWallpaper(String name, String callingPackage,
+            Rect cropHint, Bundle extras, int which, IWallpaperManagerCallback completion) {
         checkPermission(android.Manifest.permission.SET_WALLPAPER);
 
         if (which == 0) {
@@ -881,6 +1004,17 @@
             return null;
         }
 
+        // "null" means the no-op crop, preserving the full input image
+        if (cropHint == null) {
+            cropHint = new Rect(0, 0, 0, 0);
+        } else {
+            if (cropHint.isEmpty()
+                    || cropHint.left < 0
+                    || cropHint.top < 0) {
+                return null;
+            }
+        }
+
         synchronized (mLock) {
             if (DEBUG) Slog.v(TAG, "setWallpaper");
             int userId = UserHandle.getCallingUserId();
@@ -890,6 +1024,8 @@
                 ParcelFileDescriptor pfd = updateWallpaperBitmapLocked(name, wallpaper, extras);
                 if (pfd != null) {
                     wallpaper.imageWallpaperPending = true;
+                    wallpaper.setComplete = completion;
+                    wallpaper.cropHint.set(cropHint);
                 }
                 return pfd;
             } finally {
@@ -1196,6 +1332,12 @@
             out.attribute(null, "id", Integer.toString(wallpaper.wallpaperId));
             out.attribute(null, "width", Integer.toString(wallpaper.width));
             out.attribute(null, "height", Integer.toString(wallpaper.height));
+
+            out.attribute(null, "cropLeft", Integer.toString(wallpaper.cropHint.left));
+            out.attribute(null, "cropTop", Integer.toString(wallpaper.cropHint.top));
+            out.attribute(null, "cropRight", Integer.toString(wallpaper.cropHint.right));
+            out.attribute(null, "cropBottom", Integer.toString(wallpaper.cropHint.bottom));
+
             if (wallpaper.padding.left != 0) {
                 out.attribute(null, "paddingLeft", Integer.toString(wallpaper.padding.left));
             }
@@ -1208,6 +1350,7 @@
             if (wallpaper.padding.bottom != 0) {
                 out.attribute(null, "paddingBottom", Integer.toString(wallpaper.padding.bottom));
             }
+
             out.attribute(null, "name", wallpaper.name);
             if (wallpaper.wallpaperComponent != null
                     && !wallpaper.wallpaperComponent.equals(mImageWallpaper)) {
@@ -1304,6 +1447,10 @@
                         wallpaper.width = Integer.parseInt(parser.getAttributeValue(null, "width"));
                         wallpaper.height = Integer.parseInt(parser
                                 .getAttributeValue(null, "height"));
+                        wallpaper.cropHint.left = getAttributeInt(parser, "cropLeft", 0);
+                        wallpaper.cropHint.top = getAttributeInt(parser, "cropTop", 0);
+                        wallpaper.cropHint.right = getAttributeInt(parser, "cropRight", 0);
+                        wallpaper.cropHint.bottom = getAttributeInt(parser, "cropBottom", 0);
                         wallpaper.padding.left = getAttributeInt(parser, "paddingLeft", 0);
                         wallpaper.padding.top = getAttributeInt(parser, "paddingTop", 0);
                         wallpaper.padding.right = getAttributeInt(parser, "paddingRight", 0);
@@ -1322,6 +1469,7 @@
                         if (DEBUG) {
                             Slog.v(TAG, "mWidth:" + wallpaper.width);
                             Slog.v(TAG, "mHeight:" + wallpaper.height);
+                            Slog.v(TAG, "cropRect:" + wallpaper.cropHint);
                             Slog.v(TAG, "mName:" + wallpaper.name);
                             Slog.v(TAG, "mNextWallpaperComponent:"
                                     + wallpaper.nextWallpaperComponent);
@@ -1348,6 +1496,7 @@
         if (!success) {
             wallpaper.width = -1;
             wallpaper.height = -1;
+            wallpaper.cropHint.set(0, 0, 0, 0);
             wallpaper.padding.set(0, 0, 0, 0);
             wallpaper.name = "";
         } else {
@@ -1368,6 +1517,11 @@
         if (wallpaper.height < baseSize) {
             wallpaper.height = baseSize;
         }
+        // and crop, if not previously specified
+        if (wallpaper.cropHint.width() <= 0
+                || wallpaper.cropHint.height() <= 0) {
+            wallpaper.cropHint.set(0, 0, wallpaper.width, wallpaper.height);
+        }
     }
 
     private int getMaximumSizeDimension() {
@@ -1431,6 +1585,7 @@
         }
     }
 
+    // Restore the named resource bitmap to both source + crop files
     boolean restoreNamedResourceLocked(WallpaperData wallpaper) {
         if (wallpaper.name.length() > 4 && "res:".equals(wallpaper.name.substring(0, 4))) {
             String resName = wallpaper.name.substring(4);
@@ -1456,6 +1611,7 @@
                 int resId = -1;
                 InputStream res = null;
                 FileOutputStream fos = null;
+                FileOutputStream cos = null;
                 try {
                     Context c = mContext.createPackageContext(pkg, Context.CONTEXT_RESTRICTED);
                     Resources r = c.getResources();
@@ -1469,13 +1625,16 @@
                     res = r.openRawResource(resId);
                     if (wallpaper.wallpaperFile.exists()) {
                         wallpaper.wallpaperFile.delete();
+                        wallpaper.cropFile.delete();
                     }
                     fos = new FileOutputStream(wallpaper.wallpaperFile);
+                    cos = new FileOutputStream(wallpaper.cropFile);
 
                     byte[] buffer = new byte[32768];
                     int amt;
                     while ((amt=res.read(buffer)) > 0) {
                         fos.write(buffer, 0, amt);
+                        cos.write(buffer, 0, amt);
                     }
                     // mWallpaperObserver will notice the close and send the change broadcast
 
@@ -1491,8 +1650,12 @@
                     IoUtils.closeQuietly(res);
                     if (fos != null) {
                         FileUtils.sync(fos);
-                        IoUtils.closeQuietly(fos);
                     }
+                    if (cos != null) {
+                        FileUtils.sync(cos);
+                    }
+                    IoUtils.closeQuietly(fos);
+                    IoUtils.closeQuietly(cos);
                 }
             }
         }
@@ -1520,6 +1683,7 @@
                     pw.print(wallpaper.width);
                     pw.print(" mHeight=");
                     pw.println(wallpaper.height);
+                pw.print("  mCropHint="); pw.println(wallpaper.cropHint);
                 pw.print("  mPadding="); pw.println(wallpaper.padding);
                 pw.print("  mName=");  pw.println(wallpaper.name);
                 pw.print("  mWallpaperComponent="); pw.println(wallpaper.wallpaperComponent);