Use tiles for bookmarks

- Use tile based bitmap for bookmarks and history
- Settings UI cleanup

Change-Id: If959cb0b8f110035b8dd2fefe8106e9c5d30f4f1
diff --git a/src/com/android/browser/SiteTileView.java b/src/com/android/browser/SiteTileView.java
new file mode 100644
index 0000000..36f21c9
--- /dev/null
+++ b/src/com/android/browser/SiteTileView.java
@@ -0,0 +1,510 @@
+/*
+ * Copyright (c) 2015, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above
+ *       copyright notice, this list of conditions and the following
+ *       disclaimer in the documentation and/or other materials provided
+ *       with the distribution.
+ *     * Neither the name of The Linux Foundation nor the names of its
+ *       contributors may be used to endorse or promote products derived
+ *       from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+package com.android.browser;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.View;
+
+/**
+ * This represents a WebSite Tile that is created from a Drawable and will scale across any
+ * area this is externally layouted to. There are 3 possible looks:
+ *   - just the favicon (TYPE_SMALL)
+ *   - drop-shadow plus a thin overlay border (1dp) (TYPE_MEDIUM)
+ *   - centered favicon, extended color, rounded base (TYPE_LARGE)
+ *
+ * By centralizing everything in this class we make customization of looks much easier.
+ *
+ * NOTES:
+ *   - do not set a background from the outside; this overrides it automatically
+ */
+public class SiteTileView extends View {
+
+    // external configuration constants
+    public static final int TYPE_SMALL = 1;
+    public static final int TYPE_MEDIUM = 2;
+    public static final int TYPE_LARGE = 3;
+    private static final int TYPE_AUTO = 0;
+    private static final int COLOR_AUTO = 0;
+
+
+    // static configuration
+    private static final int THRESHOLD_MEDIUM_DP = 32;
+    private static final int THRESHOLD_LARGE_DP = 64;
+    private static final int LARGE_FAVICON_SIZE_DP = 48;
+    private static final int BACKGROUND_DRAWABLE_RES = R.drawable.img_tile_background;
+    private static final float FILLER_RADIUS_DP = 2f; // sync with the bg image radius
+    private static final int FILLER_FALLBACK_COLOR = Color.WHITE; // in case there is no favicon
+    private static final int OVERLINE_WIDTH_RES = R.dimen.SiteTileOverline;
+    private static final int OVERLINE_COLOR_RES = R.color.SiteTileOverline;
+
+
+    // configuration
+    private Bitmap mFaviconBitmap = null;
+    private Paint mFundamentalPaint = null;
+    private int mFaviconWidth = 0;
+    private int mFaviconHeight = 0;
+    private int mForcedType = TYPE_AUTO;
+    private int mForcedFundamentalColor = COLOR_AUTO;
+
+    // static objects, to be recycled amongst instances (this is an optimization)
+    private static int sMediumPxThreshold = -1;
+    private static int sLargePxThreshold = -1;
+    private static int sLargeFaviconPx = -1;
+    private static float sRoundedRadius = -1;
+    private static Paint sBitmapPaint = null;
+    private static Rect sSrcRect = new Rect();
+    private static Rect sDstRect = new Rect();
+    private static RectF sRectF = new RectF();
+    private static Paint sOverlineOutlinePaint = null;
+    private static Drawable sBackgroundDrawable = null;
+    private static Rect sBackgroundDrawablePadding = new Rect();
+
+    // runtime params set on Layout
+    private int mCurrentWidth = 0;
+    private int mCurrentHeight = 0;
+    private int mCurrentType = TYPE_MEDIUM;
+    private boolean mCurrentBackgroundDrawn = false;
+    private boolean mFloating = false;
+    private int mPaddingLeft = 0;
+    private int mPaddingTop = 0;
+    private int mPaddingRight = 0;
+    private int mPaddingBottom = 0;
+
+
+
+    /* XML constructors */
+
+    public SiteTileView(Context context) {
+        super(context);
+        xmlInit(null, 0);
+    }
+
+    public SiteTileView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        xmlInit(attrs, 0);
+    }
+
+    public SiteTileView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        xmlInit(attrs, defStyle);
+    }
+
+
+    /* Programmatic Constructors */
+
+    public SiteTileView(Context context, Bitmap favicon) {
+        super(context);
+        init(favicon, COLOR_AUTO);
+    }
+
+    public SiteTileView(Context context, Bitmap favicon, int fundamentalColor) {
+        super(context);
+        init(favicon, fundamentalColor);
+    }
+
+
+    /**
+     * Changes the current favicon (and associated fundamental color) on the fly
+     */
+    public void replaceFavicon(Bitmap favicon) {
+        replaceFavicon(favicon, COLOR_AUTO);
+    }
+
+    /**
+     * Changes the current favicon (and associated fundamental color) on the fly
+     * @param favicon the new favicon
+     * @param fundamentalColor the new fudamental color, or COLOR_AUTO
+     */
+    public void replaceFavicon(Bitmap favicon, int fundamentalColor) {
+        init(favicon, fundamentalColor);
+        requestLayout();
+    }
+
+    /**
+     * Disables the automatic background and filling. Useful for things that are not really
+     * "Website Tiles", like folders.
+     * @param floating true to disable the background (defaults to false)
+     */
+    public void setFloating(boolean floating) {
+        mFloating = floating;
+        invalidate();
+    }
+
+
+    /**
+     * @return The fundamental color representing the site.
+     */
+    public int getFundamentalColor() {
+        if (mForcedFundamentalColor != COLOR_AUTO)
+            return mForcedFundamentalColor;
+        if (mFundamentalPaint == null)
+            mFundamentalPaint = createFundamentalPaint(mFaviconBitmap, COLOR_AUTO);
+        return mFundamentalPaint.getColor();
+    }
+
+
+    /*** private stuff ahead ***/
+
+    private void xmlInit(AttributeSet attrs, int defStyle) {
+        // load attributes
+        final TypedArray a = getContext().obtainStyledAttributes(attrs,
+                R.styleable.SiteTileView, defStyle, 0);
+
+        // fetch the drawable, if defined - then just extract and use the bitmap
+        final Drawable drawable = a.getDrawable(R.styleable.SiteTileView_android_src);
+        final Bitmap favicon = drawable instanceof BitmapDrawable ?
+                ((BitmapDrawable) drawable).getBitmap() : null;
+
+        // check if we disable shading (plain favicon)
+        if (a.getBoolean(R.styleable.SiteTileView_flat, false))
+            mForcedType = TYPE_SMALL;
+
+        // check if we want it floating (disable shadow and filler)
+        if (a.getBoolean(R.styleable.SiteTileView_floating, false))
+            mFloating = true;
+
+        // delete attribute resolution
+        a.recycle();
+
+        // proceed with real initialization
+        init(favicon, COLOR_AUTO);
+    }
+
+    private void init(Bitmap favicon, int fundamentalColor) {
+        mFaviconBitmap = favicon;
+        if (mFaviconBitmap != null) {
+            mFaviconWidth = mFaviconBitmap.getWidth();
+            mFaviconHeight = mFaviconBitmap.getHeight();
+        }
+
+        // don't compute the paint right now, just save any hint for later
+        mFundamentalPaint = null;
+        mForcedFundamentalColor = fundamentalColor;
+
+        // shared (static) resources initialization; except for background, inited on-demand
+        if (sMediumPxThreshold < 0) {
+            final DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
+
+            // heuristics thresholds
+            sMediumPxThreshold = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+                    THRESHOLD_MEDIUM_DP, displayMetrics);
+            sLargePxThreshold = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+                    THRESHOLD_LARGE_DP, displayMetrics);
+            sLargeFaviconPx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+                    LARGE_FAVICON_SIZE_DP, displayMetrics);
+
+            // rounded radius
+            sRoundedRadius = FILLER_RADIUS_DP > 0 ? TypedValue.applyDimension(
+                    TypedValue.COMPLEX_UNIT_DIP, FILLER_RADIUS_DP, displayMetrics) : 0;
+
+            // bitmap paint (copy, smooth scale)
+            sBitmapPaint = new Paint();
+            sBitmapPaint.setColor(Color.BLACK);
+            sBitmapPaint.setFilterBitmap(true);
+
+            // overline configuration (null if we don't need it)
+            int ovlColor = getResources().getColor(OVERLINE_COLOR_RES);
+            float ovlWidthPx = getResources().getDimension(OVERLINE_WIDTH_RES);
+            if (ovlWidthPx > 0.5 && ovlColor != Color.TRANSPARENT) {
+                sOverlineOutlinePaint = new Paint();
+                sOverlineOutlinePaint.setColor(ovlColor);
+                sOverlineOutlinePaint.setStrokeWidth(ovlWidthPx);
+                sOverlineOutlinePaint.setStyle(Paint.Style.STROKE);
+            }
+        }
+
+        // change when clicked
+        setClickable(true);
+        // disable by default the long click
+        setLongClickable(false);
+    }
+
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        mCurrentWidth = right - left;
+        mCurrentHeight = bottom - top;
+
+        // auto-determine the "TYPE_" from the physical size of the layout
+        if (mForcedType == TYPE_AUTO) {
+            if (mCurrentWidth < sMediumPxThreshold && mCurrentHeight < sMediumPxThreshold)
+                mCurrentType = TYPE_SMALL;
+            else if (mCurrentWidth < sLargePxThreshold && mCurrentHeight < sLargePxThreshold)
+                mCurrentType = TYPE_MEDIUM;
+            else
+                mCurrentType = TYPE_LARGE;
+        } else {
+            // or use the forced one, if defined
+            mCurrentType = mForcedType;
+        }
+
+        // set or remove the background (if the need changed!)
+        boolean requiresBackground = mCurrentType >= TYPE_MEDIUM;
+        if (requiresBackground && !mCurrentBackgroundDrawn) {
+            // draw the background
+            mCurrentBackgroundDrawn = true;
+
+            // load the background just the first time, on demand (it may fail too)
+            if (sBackgroundDrawable == null) {
+                sBackgroundDrawable = getResources().getDrawable(BACKGROUND_DRAWABLE_RES);
+                if (sBackgroundDrawable != null)
+                    sBackgroundDrawable.getPadding(sBackgroundDrawablePadding);
+            }
+
+            // background -> padding
+            mPaddingLeft = sBackgroundDrawablePadding.left;
+            mPaddingTop = sBackgroundDrawablePadding.top;
+            mPaddingRight = sBackgroundDrawablePadding.right;
+            mPaddingBottom = sBackgroundDrawablePadding.bottom;
+        } else if (!requiresBackground && mCurrentBackgroundDrawn) {
+            // turn off background drawing
+            mCurrentBackgroundDrawn = false;
+
+            // no background -> no padding
+            mPaddingLeft = 0;
+            mPaddingTop = 0;
+            mPaddingRight = 0;
+            mPaddingBottom = 0;
+        }
+
+        // just proceed, do nothing here
+        super.onLayout(changed, left, top, right, bottom);
+    }
+
+    @Override
+    public void setPressed(boolean pressed) {
+        super.setPressed(pressed);
+        // schedule a repaint to show pressed/released
+        invalidate();
+    }
+
+    @Override
+    public void setSelected(boolean selected) {
+        super.setSelected(selected);
+        // schedule a repaint to show selected
+        invalidate();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        // Selection State: make everything smaller
+        if (isSelected()) {
+            float scale = 0.8f;
+            canvas.translate(mCurrentWidth * (1 - scale) / 2, mCurrentHeight * (1 - scale) / 2);
+            canvas.scale(scale, scale);
+        }
+
+        // Pressed state: make the button reach the finger
+        if (isPressed()) {
+            float scale = 1.1f;
+            canvas.translate(mCurrentWidth * (1 - scale) / 2, mCurrentHeight * (1 - scale) / 2);
+            canvas.scale(scale, scale);
+        }
+
+        final int left = mPaddingLeft;
+        final int top = mPaddingTop;
+        final int right = mCurrentWidth - mPaddingRight;
+        final int bottom = mCurrentHeight - mPaddingBottom;
+        final int contentWidth = right - left;
+        final int contentHeight = bottom - top;
+
+        // A. the background drawable (if set)
+        boolean requiresBackground = mCurrentBackgroundDrawn && sBackgroundDrawable != null
+                && !isPressed() && !mFloating;
+        if (requiresBackground) {
+            sBackgroundDrawable.setBounds(0, 0, mCurrentWidth, mCurrentHeight);
+            sBackgroundDrawable.draw(canvas);
+        }
+
+        // B. (when needed) draw the background rectangle; sharp our rounded
+        boolean requiresFundamentalFiller = mCurrentType >= TYPE_LARGE && !mFloating;
+        if (requiresFundamentalFiller) {
+            // create the filler paint on demand (not all icons need it)
+            if (mFundamentalPaint == null)
+                mFundamentalPaint = createFundamentalPaint(mFaviconBitmap, mForcedFundamentalColor);
+
+            // paint if not white, since requiresBackground already painted it white
+            int fundamentalColor = mFundamentalPaint.getColor();
+            if (fundamentalColor != COLOR_AUTO &&
+                    (fundamentalColor != Color.WHITE || !requiresBackground)) {
+                if (sRoundedRadius >= 1.) {
+                    sRectF.set(left, top, right, bottom);
+                    canvas.drawRoundRect(sRectF, sRoundedRadius, sRoundedRadius, mFundamentalPaint);
+                } else
+                    canvas.drawRect(left, top, right, bottom, mFundamentalPaint);
+            }
+        }
+
+        // C. (if present) draw the favicon
+        boolean requiresFavicon = mFaviconBitmap != null
+                && mFaviconWidth > 1 && mFaviconHeight > 1;
+        if (requiresFavicon) {
+            // destination can either fill, or auto-center
+            boolean fillSpace = mCurrentType <= TYPE_MEDIUM;
+            if (fillSpace || contentWidth < sLargeFaviconPx || contentHeight < sLargeFaviconPx) {
+                sDstRect.set(left, top, right, bottom);
+            } else {
+                int dstLeft = left + (contentWidth - sLargeFaviconPx) / 2;
+                int dstTop = top + (contentHeight - sLargeFaviconPx) / 2;
+                sDstRect.set(dstLeft, dstTop, dstLeft + sLargeFaviconPx, dstTop + sLargeFaviconPx);
+            }
+
+            // source has to 'crop proportionally' to keep the dest aspect ratio
+            sSrcRect.set(0, 0, mFaviconWidth, mFaviconHeight);
+            int sW = sSrcRect.width();
+            int sH = sSrcRect.height();
+            int dW = sDstRect.width();
+            int dH = sDstRect.height();
+            if (sW > 4 && sH > 4 && dW > 4 && dH > 4) {
+                float hScale = (float) dW / (float) sW;
+                float vScale = (float) dH / (float) sH;
+                if (hScale == vScale) {
+                    // no transformation needed, just zoom
+                } else if (hScale < vScale) {
+                    // horizontal crop
+                    float hCrop = 1 - hScale / vScale;
+                    int hCropPx = (int) (sW * hCrop / 2 + 0.5);
+                    sSrcRect.left += hCropPx;
+                    sSrcRect.right -= hCropPx;
+                    canvas.drawBitmap(mFaviconBitmap, sSrcRect, sDstRect, sBitmapPaint);
+                } else {
+                    // vertical crop
+                    float vCrop = 1 - vScale / hScale;
+                    int vCropPx = (int) (sH * vCrop / 2 + 0.5);
+                    sSrcRect.top += vCropPx;
+                    sSrcRect.bottom -= vCropPx;
+                }
+            }
+
+            // blit favicon, croppped, scaled
+            canvas.drawBitmap(mFaviconBitmap, sSrcRect, sDstRect, sBitmapPaint);
+        }
+
+        // D. (when needed) draw the thin over-line
+        boolean requiresOverline = mCurrentType == TYPE_MEDIUM
+                && sOverlineOutlinePaint != null;
+        if (requiresOverline) {
+            canvas.drawRect(left, top, right, bottom, sOverlineOutlinePaint);
+        }
+
+        /*if (true) { // DEBUG TYPE
+            Paint paint = new Paint();
+            paint.setColor(Color.BLACK);
+            paint.setTextSize(20);
+            canvas.drawText(String.valueOf(mCurrentType), 30, 30, paint);
+        }*/
+    }
+
+
+    /**
+     * Creates a fill Paint from the favicon, or using the forced color (if not COLOR_AUTO)
+     */
+    private static Paint createFundamentalPaint(Bitmap favicon, int forceFillColor) {
+        final Paint fillPaint = new Paint();
+        if (forceFillColor != COLOR_AUTO)
+            fillPaint.setColor(forceFillColor);
+        else
+            fillPaint.setColor(guessFundamentalColor(favicon));
+        return fillPaint;
+    }
+
+    /**
+     * This uses very stupid mechanism - a 9x9 grid sample on the borders and center - and selects
+     * the color with the most frequency, or the center.
+     *
+     * @param bitmap the bitmap to guesss the color about
+     * @return a Color
+     */
+    private static int guessFundamentalColor(Bitmap bitmap) {
+        if (bitmap == null)
+            return FILLER_FALLBACK_COLOR;
+        int height = bitmap.getHeight();
+        int width = bitmap.getWidth();
+        if (height < 2 || width < 2)
+            return FILLER_FALLBACK_COLOR;
+
+        // pick up to 9 colors
+        // NOTE: the order of sampling sets the precendece, in case of ties
+        int[] pxColors = new int[9];
+        int idx = 0;
+        if ((pxColors[idx] = sampleColor(bitmap, width / 2, height / 2)) != 0) idx++;
+        if ((pxColors[idx] = sampleColor(bitmap, width / 2, height - 1)) != 0) idx++;
+        if ((pxColors[idx] = sampleColor(bitmap, width - 1, height - 1)) != 0) idx++;
+        if ((pxColors[idx] = sampleColor(bitmap, width - 1, height / 2)) != 0) idx++;
+        if ((pxColors[idx] = sampleColor(bitmap,         0, 0         )) != 0) idx++;
+        if ((pxColors[idx] = sampleColor(bitmap, width / 2, 0         )) != 0) idx++;
+        if ((pxColors[idx] = sampleColor(bitmap, width - 1, 0         )) != 0) idx++;
+        if ((pxColors[idx] = sampleColor(bitmap, 0        , height / 2)) != 0) idx++;
+        if ((pxColors[idx] = sampleColor(bitmap, 0        , height - 1)) != 0) idx++;
+
+        // find the most popular
+        int popColor = -1;
+        int popCount = -1;
+        for (int i = 0; i < idx; i++) {
+            int thisColor = pxColors[i];
+            int thisCount = 0;
+            for (int j = 0; j < idx; j++) {
+                if (pxColors[j] == thisColor)
+                    thisCount++;
+            }
+            if (thisCount > popCount) {
+                popColor = thisColor;
+                popCount = thisCount;
+            }
+        }
+        return popCount > -1 ? popColor : FILLER_FALLBACK_COLOR;
+    }
+
+    /**
+     * @return Color, but if it's 0, you should discard it (not representative)
+     */
+    private static int sampleColor(Bitmap bitmap, int x, int y) {
+        int color = bitmap.getPixel(x, y);
+        // discard semi-transparent pixels, because they're probably from a spurious border
+        // discard black pixels, because black is not a color (well, not a good looking one)
+        if ((color >>> 24) <= 128 || (color & 0xFFFFFF) == 0)
+            return 0;
+        return color;
+    }
+
+}
\ No newline at end of file