Icon class should support Maskable bitmap type

Test: Unit test on IconTest
$ runtest --path=frameworks/base/graphics/tests/graphicstests/src/android/graphics/drawable/IconTest.java

b/34196580

Change-Id: I321c4b02f17ad9426c053216c4c88616a605aacf
diff --git a/api/current.txt b/api/current.txt
index d772181..ea0ed46 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -13766,6 +13766,7 @@
     method public static android.graphics.drawable.Icon createWithContentUri(android.net.Uri);
     method public static android.graphics.drawable.Icon createWithData(byte[], int, int);
     method public static android.graphics.drawable.Icon createWithFilePath(java.lang.String);
+    method public static android.graphics.drawable.Icon createWithMaskableBitmap(android.graphics.Bitmap);
     method public static android.graphics.drawable.Icon createWithResource(android.content.Context, int);
     method public static android.graphics.drawable.Icon createWithResource(java.lang.String, int);
     method public int describeContents();
@@ -13849,9 +13850,9 @@
   }
 
   public class MaskableIconDrawable extends android.graphics.drawable.Drawable implements android.graphics.drawable.Drawable.Callback {
-    ctor public MaskableIconDrawable(android.graphics.drawable.Drawable, android.graphics.drawable.Drawable);
     method public void draw(android.graphics.Canvas);
     method public android.graphics.drawable.Drawable getBackground();
+    method public static float getExtraInsetPercentage();
     method public android.graphics.drawable.Drawable getForeground();
     method public android.graphics.Path getIconMask();
     method public int getOpacity();
@@ -13861,8 +13862,6 @@
     method public void setColorFilter(android.graphics.ColorFilter);
     method public void setOpacity(int);
     method public void unscheduleDrawable(android.graphics.drawable.Drawable, java.lang.Runnable);
-    field public static final float DEFAULT_VIEW_PORT_SCALE = 0.6666667f;
-    field public static final float MASK_SIZE = 100.0f;
   }
 
   public class NinePatchDrawable extends android.graphics.drawable.Drawable {
diff --git a/api/system-current.txt b/api/system-current.txt
index 6affa7f..a0c296a 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -14334,6 +14334,7 @@
     method public static android.graphics.drawable.Icon createWithContentUri(android.net.Uri);
     method public static android.graphics.drawable.Icon createWithData(byte[], int, int);
     method public static android.graphics.drawable.Icon createWithFilePath(java.lang.String);
+    method public static android.graphics.drawable.Icon createWithMaskableBitmap(android.graphics.Bitmap);
     method public static android.graphics.drawable.Icon createWithResource(android.content.Context, int);
     method public static android.graphics.drawable.Icon createWithResource(java.lang.String, int);
     method public int describeContents();
@@ -14417,9 +14418,9 @@
   }
 
   public class MaskableIconDrawable extends android.graphics.drawable.Drawable implements android.graphics.drawable.Drawable.Callback {
-    ctor public MaskableIconDrawable(android.graphics.drawable.Drawable, android.graphics.drawable.Drawable);
     method public void draw(android.graphics.Canvas);
     method public android.graphics.drawable.Drawable getBackground();
+    method public static float getExtraInsetPercentage();
     method public android.graphics.drawable.Drawable getForeground();
     method public android.graphics.Path getIconMask();
     method public int getOpacity();
@@ -14429,8 +14430,6 @@
     method public void setColorFilter(android.graphics.ColorFilter);
     method public void setOpacity(int);
     method public void unscheduleDrawable(android.graphics.drawable.Drawable, java.lang.Runnable);
-    field public static final float DEFAULT_VIEW_PORT_SCALE = 0.6666667f;
-    field public static final float MASK_SIZE = 100.0f;
   }
 
   public class NinePatchDrawable extends android.graphics.drawable.Drawable {
diff --git a/api/test-current.txt b/api/test-current.txt
index bbb7f89..6d009f1 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -13800,6 +13800,7 @@
     method public static android.graphics.drawable.Icon createWithContentUri(android.net.Uri);
     method public static android.graphics.drawable.Icon createWithData(byte[], int, int);
     method public static android.graphics.drawable.Icon createWithFilePath(java.lang.String);
+    method public static android.graphics.drawable.Icon createWithMaskableBitmap(android.graphics.Bitmap);
     method public static android.graphics.drawable.Icon createWithResource(android.content.Context, int);
     method public static android.graphics.drawable.Icon createWithResource(java.lang.String, int);
     method public int describeContents();
@@ -13883,20 +13884,19 @@
   }
 
   public class MaskableIconDrawable extends android.graphics.drawable.Drawable implements android.graphics.drawable.Drawable.Callback {
-    ctor public MaskableIconDrawable(android.graphics.drawable.Drawable, android.graphics.drawable.Drawable);
     method public void draw(android.graphics.Canvas);
     method public android.graphics.drawable.Drawable getBackground();
+    method public static float getExtraInsetPercentage();
     method public android.graphics.drawable.Drawable getForeground();
     method public android.graphics.Path getIconMask();
     method public int getOpacity();
+    method public android.graphics.Region getSafeZone();
     method public void invalidateDrawable(android.graphics.drawable.Drawable);
     method public void scheduleDrawable(android.graphics.drawable.Drawable, java.lang.Runnable, long);
     method public void setAlpha(int);
     method public void setColorFilter(android.graphics.ColorFilter);
     method public void setOpacity(int);
     method public void unscheduleDrawable(android.graphics.drawable.Drawable, java.lang.Runnable);
-    field public static final float DEFAULT_VIEW_PORT_SCALE = 0.6666667f;
-    field public static final float MASK_SIZE = 100.0f;
   }
 
   public class NinePatchDrawable extends android.graphics.drawable.Drawable {
diff --git a/graphics/java/android/graphics/drawable/Icon.java b/graphics/java/android/graphics/drawable/Icon.java
index 9772009..60c3b1c 100644
--- a/graphics/java/android/graphics/drawable/Icon.java
+++ b/graphics/java/android/graphics/drawable/Icon.java
@@ -67,6 +67,8 @@
     public static final int TYPE_DATA     = 3;
     /** @hide */
     public static final int TYPE_URI      = 4;
+    /** @hide */
+    public static final int TYPE_BITMAP_MASKABLE      = 5;
 
     private static final int VERSION_STREAM_SERIALIZER = 1;
 
@@ -101,6 +103,7 @@
      * {@link #TYPE_RESOURCE},
      * {@link #TYPE_DATA}, or
      * {@link #TYPE_URI}.
+     * {@link #TYPE_BITMAP_MASKABLE}
      * @hide
      */
     public int getType() {
@@ -112,7 +115,7 @@
      * @hide
      */
     public Bitmap getBitmap() {
-        if (mType != TYPE_BITMAP) {
+        if (mType != TYPE_BITMAP && mType != TYPE_BITMAP_MASKABLE) {
             throw new IllegalStateException("called getBitmap() on " + this);
         }
         return (Bitmap) mObj1;
@@ -218,6 +221,7 @@
     private static final String typeToString(int x) {
         switch (x) {
             case TYPE_BITMAP: return "BITMAP";
+            case TYPE_BITMAP_MASKABLE: return "BITMAP_MASKABLE";
             case TYPE_DATA: return "DATA";
             case TYPE_RESOURCE: return "RESOURCE";
             case TYPE_URI: return "URI";
@@ -285,6 +289,9 @@
         switch (mType) {
             case TYPE_BITMAP:
                 return new BitmapDrawable(context.getResources(), getBitmap());
+            case TYPE_BITMAP_MASKABLE:
+                return new MaskableIconDrawable(null,
+                    new BitmapDrawable(context.getResources(), getBitmap()));
             case TYPE_RESOURCE:
                 if (getResources() == null) {
                     // figure out where to load resources from
@@ -388,7 +395,7 @@
      * @hide
      */
     public void convertToAshmem() {
-        if (mType == TYPE_BITMAP &&
+        if ((mType == TYPE_BITMAP || mType == TYPE_BITMAP_MASKABLE) &&
             getBitmap().isMutable() &&
             getBitmap().getAllocationByteCount() >= MIN_ASHMEM_ICON_SIZE) {
             setBitmap(getBitmap().createAshmemBitmap());
@@ -409,6 +416,7 @@
 
         switch (mType) {
             case TYPE_BITMAP:
+            case TYPE_BITMAP_MASKABLE:
                 getBitmap().compress(Bitmap.CompressFormat.PNG, 100, dataStream);
                 break;
             case TYPE_DATA:
@@ -444,6 +452,8 @@
             switch (type) {
                 case TYPE_BITMAP:
                     return createWithBitmap(BitmapFactory.decodeStream(inputStream));
+                case TYPE_BITMAP_MASKABLE:
+                    return createWithMaskableBitmap(BitmapFactory.decodeStream(inputStream));
                 case TYPE_DATA:
                     final int length = inputStream.readInt();
                     final byte[] data = new byte[length];
@@ -478,6 +488,7 @@
         }
         switch (mType) {
             case TYPE_BITMAP:
+            case TYPE_BITMAP_MASKABLE:
                 return getBitmap() == otherIcon.getBitmap();
             case TYPE_DATA:
                 return getDataLength() == otherIcon.getDataLength()
@@ -551,6 +562,20 @@
     }
 
     /**
+     * Create an Icon pointing to a bitmap in memory that follows the icon design guideline defined
+     * by {@link MaskableIconDrawable}.
+     * @param bits A valid {@link android.graphics.Bitmap} object
+     */
+    public static Icon createWithMaskableBitmap(Bitmap bits) {
+        if (bits == null) {
+            throw new IllegalArgumentException("Bitmap must not be null.");
+        }
+        final Icon rep = new Icon(TYPE_BITMAP_MASKABLE);
+        rep.setBitmap(bits);
+        return rep;
+    }
+
+    /**
      * Create an Icon pointing to a compressed bitmap stored in a byte array.
      * @param data Byte array storing compressed bitmap data of a type that
      *             {@link android.graphics.BitmapFactory}
@@ -654,6 +679,7 @@
         final StringBuilder sb = new StringBuilder("Icon(typ=").append(typeToString(mType));
         switch (mType) {
             case TYPE_BITMAP:
+            case TYPE_BITMAP_MASKABLE:
                 sb.append(" size=")
                         .append(getBitmap().getWidth())
                         .append("x")
@@ -692,7 +718,7 @@
      * Parcelable interface
      */
     public int describeContents() {
-        return (mType == TYPE_BITMAP || mType == TYPE_DATA)
+        return (mType == TYPE_BITMAP || mType == TYPE_BITMAP_MASKABLE || mType == TYPE_DATA)
                 ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0;
     }
 
@@ -702,6 +728,7 @@
         this(in.readInt());
         switch (mType) {
             case TYPE_BITMAP:
+            case TYPE_BITMAP_MASKABLE:
                 final Bitmap bits = Bitmap.CREATOR.createFromParcel(in);
                 mObj1 = bits;
                 break;
@@ -740,6 +767,7 @@
         dest.writeInt(mType);
         switch (mType) {
             case TYPE_BITMAP:
+            case TYPE_BITMAP_MASKABLE:
                 final Bitmap bits = getBitmap();
                 getBitmap().writeToParcel(dest, flags);
                 break;
diff --git a/graphics/java/android/graphics/drawable/MaskableIconDrawable.java b/graphics/java/android/graphics/drawable/MaskableIconDrawable.java
index 043f092..e4f1788a 100644
--- a/graphics/java/android/graphics/drawable/MaskableIconDrawable.java
+++ b/graphics/java/android/graphics/drawable/MaskableIconDrawable.java
@@ -20,6 +20,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.TestApi;
 import android.content.pm.ActivityInfo.Config;
 import android.content.res.ColorStateList;
 import android.content.res.Resources;
@@ -62,17 +63,22 @@
 
     /**
      * Mask path is defined inside device configuration in following dimension: [100 x 100]
+     * @hide
      */
     public static final float MASK_SIZE = 100f;
+    private static final float SAFEZONE_SCALE = .9f;
 
     /**
-     * The view port of the layers is smaller than their intrinsic width and height by this factor.
-     *
-     * It is part of the API contract that all four sides of the layers are padded so as to provide
+     * All four sides of the layers are padded with extra inset so as to provide
      * extra content to reveal within the clip path when performing affine transformations on the
      * layers.
+     *
+     * Each layers will reserve 25% of it's width and height.
+     *
+     * As a result, the view port of the layers is smaller than their intrinsic width and height.
      */
-    public static final float DEFAULT_VIEW_PORT_SCALE = 2f / 3f;
+    private static final float EXTRA_INSET_PERCENTAGE = 1 / 4f;
+    private static final float DEFAULT_VIEW_PORT_SCALE = 1f / (1 + 2 * EXTRA_INSET_PERCENTAGE);
 
     /**
      * Clip path defined in {@link com.android.internal.R.string.config_icon_mask}.
@@ -155,12 +161,17 @@
      *
      * @param backgroundDrawable drawable that should be rendered in the background
      * @param foregroundDrawable drawable that should be rendered in the foreground
+     * @hide
      */
     public MaskableIconDrawable(Drawable backgroundDrawable,
             Drawable foregroundDrawable) {
         this((LayerState)null, null);
-        addLayer(BACKGROUND_ID, createChildDrawable(backgroundDrawable));
-        addLayer(FOREGROUND_ID, createChildDrawable(foregroundDrawable));
+        if (backgroundDrawable != null) {
+            addLayer(BACKGROUND_ID, createChildDrawable(backgroundDrawable));
+        }
+        if (foregroundDrawable != null) {
+            addLayer(FOREGROUND_ID, createChildDrawable(foregroundDrawable));
+        }
     }
 
     /**
@@ -199,6 +210,15 @@
     }
 
     /**
+     * All four sides of the layers are padded with extra inset so as to provide
+     * extra content to reveal within the clip path when performing affine transformations on the
+     * layers.
+     */
+    public static float getExtraInsetPercentage() {
+        return EXTRA_INSET_PERCENTAGE;
+    }
+
+    /**
      * @return the mask path object used to clip the drawable
      */
     public Path getIconMask() {
@@ -242,13 +262,20 @@
         int cY = bounds.centerY();
 
         for (int i = 0, count = mLayerState.N_CHILDREN; i < count; i++) {
+            final ChildDrawable r = mLayerState.mChildren[i];
+            if (r == null) {
+                continue;
+            }
+            final Drawable d = r.mDrawable;
+            if (d == null) {
+                continue;
+            }
+
             int insetWidth = (int) (bounds.width() / (DEFAULT_VIEW_PORT_SCALE * 2));
             int insetHeight = (int) (bounds.height() / (DEFAULT_VIEW_PORT_SCALE * 2));
             final Rect outRect = mTmpOutRect;
             outRect.set(cX - insetWidth, cY - insetHeight, cX + insetWidth, cY + insetHeight);
 
-            final ChildDrawable r = mLayerState.mChildren[i];
-            final Drawable d = r.mDrawable;
             d.setBounds(outRect);
         }
     }
@@ -273,6 +300,9 @@
         if (mLayersShader == null) {
             mCanvas.setBitmap(mLayersBitmap);
             for (int i = 0; i < mLayerState.N_CHILDREN; i++) {
+                if (mLayerState.mChildren[i] == null) {
+                    continue;
+                }
                 final Drawable dr = mLayerState.mChildren[i].mDrawable;
                 if (dr != null) {
                     dr.draw(mCanvas);
@@ -295,6 +325,18 @@
         outline.setConvexPath(mMask);
     }
 
+    /** @hide */
+    @TestApi
+    public Region getSafeZone() {
+        mMaskMatrix.reset();
+        mMaskMatrix.setScale(SAFEZONE_SCALE, SAFEZONE_SCALE, getBounds().centerX(), getBounds().centerY());
+        Path p = new Path();
+        mMask.transform(mMaskMatrix, p);
+        Region safezoneRegion = new Region(getBounds());
+        safezoneRegion.setPath(p, safezoneRegion);
+        return safezoneRegion;
+    }
+
     @Override
     public @Nullable Region getTransparentRegion() {
         if (mTransparentRegion.isEmpty()) {
diff --git a/graphics/tests/graphicstests/src/android/graphics/drawable/IconTest.java b/graphics/tests/graphicstests/src/android/graphics/drawable/IconTest.java
index a214b9e..50c498b 100644
--- a/graphics/tests/graphicstests/src/android/graphics/drawable/IconTest.java
+++ b/graphics/tests/graphicstests/src/android/graphics/drawable/IconTest.java
@@ -18,6 +18,7 @@
 
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
+import android.graphics.Region;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Parcel;
@@ -108,6 +109,40 @@
     }
 
     @SmallTest
+    public void testWithMaskableBitmap() throws Exception {
+        final Bitmap bm1 = Bitmap.createBitmap(150, 150, Bitmap.Config.ARGB_8888);
+
+        final Canvas can1 = new Canvas(bm1);
+        can1.drawColor(0xFFFF0000);
+
+        final Icon im1 = Icon.createWithMaskableBitmap(bm1);
+
+        final MaskableIconDrawable draw1 = (MaskableIconDrawable) im1.loadDrawable(mContext);
+
+        final Bitmap test1 = Bitmap.createBitmap(
+            (int)(draw1.getIntrinsicWidth() * (1 + 2 * MaskableIconDrawable.getExtraInsetPercentage())),
+            (int)(draw1.getIntrinsicHeight() * (1 + 2 * MaskableIconDrawable.getExtraInsetPercentage())),
+            Bitmap.Config.ARGB_8888);
+
+        draw1.setBounds(0, 0,
+            (int) (draw1.getIntrinsicWidth() * (1 + 2 * MaskableIconDrawable.getExtraInsetPercentage())),
+            (int) (draw1.getIntrinsicHeight() * (1 + 2 * MaskableIconDrawable.getExtraInsetPercentage())));
+        draw1.draw(new Canvas(test1));
+
+        final File dir = getContext().getExternalFilesDir(null);
+        L("writing temp bitmaps to %s...", dir);
+
+        bm1.compress(Bitmap.CompressFormat.PNG, 100,
+            new FileOutputStream(new File(dir, "maskable-bitmap1-original.png")));
+        test1.compress(Bitmap.CompressFormat.PNG, 100,
+            new FileOutputStream(new File(dir, "maskable-bitmap1-test.png")));
+        if (!equalBitmaps(bm1, test1, draw1.getSafeZone())) {
+            findBitmapDifferences(bm1, test1);
+            fail("maskable bitmap1 differs, check " + dir);
+        }
+    }
+
+    @SmallTest
     public void testWithBitmapResource() throws Exception {
         final Bitmap res1 = ((BitmapDrawable) getContext().getDrawable(R.drawable.landscape))
                 .getBitmap();
@@ -294,17 +329,31 @@
         printBits(aPix, w, h);
     }
     boolean equalBitmaps(Bitmap a, Bitmap b) {
+        return equalBitmaps(a, b, null);
+    }
+
+    boolean equalBitmaps(Bitmap a, Bitmap b, Region region) {
         if (a.getWidth() != b.getWidth() || a.getHeight() != b.getHeight()) return false;
-        
+
         final int w = a.getWidth();
         final int h = a.getHeight();
         int[] aPix = new int[w * h];
         int[] bPix = new int[w * h];
 
-        a.getPixels(aPix, 0, w, 0, 0, w, h);
-        b.getPixels(bPix, 0, w, 0, 0, w, h);
-
-        return Arrays.equals(aPix, bPix);
+        if (region != null) {
+            for (int i = 0; i < w; i++) {
+                for (int j = 0; j < h; j++) {
+                    if (region.contains(i, j) && a.getPixel(i, j) != b.getPixel(i, j)) {
+                        return false;
+                    }
+                }
+            }
+            return true;
+        } else {
+            a.getPixels(aPix, 0, w, 0, 0, w, h);
+            b.getPixels(bPix, 0, w, 0, 0, w, h);
+            return Arrays.equals(aPix, bPix);
+        }
     }
 
     void findBitmapDifferences(Bitmap a, Bitmap b) {