/* * 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.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Typeface; 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; import java.util.HashMap; import java.util.Map; /** * 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 { // public trust level constants public static final int TRUST_UNKNOWN = 0; // default public static final int TRUST_AVOID = 0x01; public static final int TRUST_UNTRUSTED = 0x02; public static final int TRUST_TRUSTED = 0x04; private static final int TRUST_MASK = 0x07; // 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 int DEFAULT_SITE_FAVICON = 0; 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 boolean BADGE_SHOW_BLOCKED_COUNT = false; // internal enums private static final int TYPE_SMALL = 1; private static final int TYPE_MEDIUM = 2; private static final int TYPE_LARGE = 3; private static final int TYPE_AUTO = 0; private static final int COLOR_AUTO = 0; // configuration private Bitmap mFaviconBitmap = null; private Paint mFundamentalPaint = null; private int mFaviconWidth = 0; private int mFaviconHeight = 0; private int mForcedFundamentalColor = COLOR_AUTO; private boolean mBackgroundDisabled = false; private int mTrustLevel = TRUST_UNKNOWN; private int mBadgeBlockedObjectsCount = 0; private boolean mBadgeHasCertIssues = false; // runtime params set on Layout private int mCurrentWidth = 0; private int mCurrentHeight = 0; private int mCurrentType = TYPE_MEDIUM; private int mPaddingLeft = 0; private int mPaddingTop = 0; private int mPaddingRight = 0; private int mPaddingBottom = 0; private boolean mCurrentShadowDrawn = false; // static objects, to be recycled amongst instances (this is an optimization) // NOTE: package-visible statics are for optimized usage inside FolderTileView as well private static int sMediumPxThreshold = -1; private static int sLargePxThreshold = -1; private static int sLargeFaviconPx = -1; /* package */ static float sRoundedRadius = -1; private static Paint sBitmapPaint = null; private static Paint sBadgeTextPaint = null; private static Rect sSrcRect = new Rect(); private static Rect sDstRect = new Rect(); /* package */ static RectF sRectF = new RectF(); private static Drawable sBackgroundDrawable = null; private static class BadgeAssets { Drawable back; Drawable accent; int textColor; } private static Map sBadges; private static Bitmap sDefaultSiteBitmap = null; /* package */ static Rect sBackgroundDrawablePadding = new Rect(); /* 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 disabled true to disable the background (defaults to false) */ public void setBackgroundDisabled(boolean disabled) { if (mBackgroundDisabled != disabled) { mBackgroundDisabled = disabled; invalidate(); } } /** * This results in the Badge being updated * @param trustLevel one of the TRUST_ constants */ public void setTrustLevel(int trustLevel) { if (mTrustLevel != trustLevel) { mTrustLevel = trustLevel; invalidate(); } } /** * Tells that there will be some message about issues inside * @param certIssues true if there are issues. */ public void setBadgeHasCertIssues(boolean certIssues) { if (certIssues != mBadgeHasCertIssues) { mBadgeHasCertIssues = certIssues; invalidate(); } } /** * Sets the number of objects blocked (a positive contribution to the page). Presentation * may or may not have the number indication. * @param sessionCounter Counter of blocked objects. Use 0 to not display anything. */ public void setBadgeBlockedObjectsCount(int sessionCounter) { if (sessionCounter != mBadgeBlockedObjectsCount) { // repaint if going from or to 0, or if showing the ads count //noinspection PointlessBooleanExpression,ConstantConditions if (mBadgeBlockedObjectsCount == 0 || sessionCounter == 0 || BADGE_SHOW_BLOCKED_COUNT) invalidate(); mBadgeBlockedObjectsCount = sessionCounter; } } /** * @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 boolean requiresBadge() { return !mBackgroundDisabled && (mTrustLevel != TRUST_UNKNOWN || mBadgeHasCertIssues || mBadgeBlockedObjectsCount > 0); } private int computeBadgeMessages() { // special case, for TRUST_AVOID, always show the common accent if (mTrustLevel == TRUST_AVOID) return 0; // recompute number of 'messages' inside the badge int count = 0; if (mBadgeHasCertIssues) count++; if (mBadgeBlockedObjectsCount > 0) count++; // add the number of blocked objects (-1, for having already counted the message) if needed if (BADGE_SHOW_BLOCKED_COUNT) count += mBadgeBlockedObjectsCount - 1; return count; } 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 want it background-less (disable shadow and filler) setBackgroundDisabled(a.getBoolean(R.styleable.SiteTileView_disableBackground, false)); // read the trust level (unknown, aka 'default', if not present) setTrustLevel(a.getInteger(R.styleable.SiteTileView_trustLevel, TRUST_UNKNOWN) & TRUST_MASK); // read the amount of blocked objects (or 0 if not present) setBadgeBlockedObjectsCount(a.getInteger(R.styleable.SiteTileView_blockedObjects, 0)); // delete attribute resolution a.recycle(); // proceed with real initialization init(favicon, COLOR_AUTO); } private void init(Bitmap favicon, int fundamentalColor) { mFaviconBitmap = favicon; // show a default favicon if nothing is set (consider removing this, it's ugly) if (mFaviconBitmap == null && DEFAULT_SITE_FAVICON != 0) { if (sDefaultSiteBitmap == null) sDefaultSiteBitmap = BitmapFactory.decodeResource(getResources(), DEFAULT_SITE_FAVICON); mFaviconBitmap = sDefaultSiteBitmap; fundamentalColor = 0xFFFAFAFA; } 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 ensureCommonLoaded(getResources()); // change when clicked setClickable(true); } static void ensureCommonLoaded(Resources r) { // check if already initialized if (sMediumPxThreshold != -1) return; // heuristics thresholds final DisplayMetrics displayMetrics = r.getDisplayMetrics(); 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.WHITE); sBitmapPaint.setFilterBitmap(true); // badge text paint (anti-aliased) sBadgeTextPaint = new Paint(); sBadgeTextPaint.setAntiAlias(true); Typeface badgeTypeface = Typeface.create("sans-serif-medium", Typeface.NORMAL); if (badgeTypeface != null) sBadgeTextPaint.setTypeface(badgeTypeface); // load the background (could be loaded on demand, but in the end it's always needed) sBackgroundDrawable = r.getDrawable(BACKGROUND_DRAWABLE_RES); if (sBackgroundDrawable != null) sBackgroundDrawable.getPadding(sBackgroundDrawablePadding); // load all the badge drawables sBadges = new HashMap<>(); loadBadgeResources(r, TRUST_AVOID, R.drawable.img_deco_tile_avoid, R.drawable.img_deco_tile_avoid_accent, R.color.TileBadgeTextAvoid); loadBadgeResources(r, TRUST_UNTRUSTED, R.drawable.img_deco_tile_untrusted, R.drawable.img_deco_tile_untrusted_accent, R.color.TileBadgeTextUntrusted); loadBadgeResources(r, TRUST_UNKNOWN, R.drawable.img_deco_tile_unknown, R.drawable.img_deco_tile_unknown_accent, R.color.TileBadgeTextUnknown); loadBadgeResources(r, TRUST_TRUSTED, R.drawable.img_deco_tile_verified, R.drawable.img_deco_tile_verified_accent, R.color.TileBadgeTextVerified); } private static void loadBadgeResources(Resources r, int t, int back, int accent, int color) { BadgeAssets ba = new BadgeAssets(); ba.back = back == 0 ? null : r.getDrawable(back); ba.accent = accent == 0 ? null : r.getDrawable(accent); ba.textColor = color == 0 ? Color.TRANSPARENT : r.getColor(color); sBadges.put(t, ba); } static Rect getBackgroundDrawablePadding() { return sBackgroundDrawablePadding != null ? sBackgroundDrawablePadding : new Rect(); } @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 (mCurrentWidth < sMediumPxThreshold && mCurrentHeight < sMediumPxThreshold) mCurrentType = TYPE_SMALL; else if (mCurrentWidth < sLargePxThreshold && mCurrentHeight < sLargePxThreshold) mCurrentType = TYPE_MEDIUM; else mCurrentType = TYPE_LARGE; // set or remove the background (if the need changed!) boolean requiresBackgroundDrawable = mCurrentType >= TYPE_MEDIUM; if (requiresBackgroundDrawable && !mCurrentShadowDrawn) { // draw the background mCurrentShadowDrawn = mCurrentType >= TYPE_LARGE; // background -> padding mPaddingLeft = sBackgroundDrawablePadding.left; mPaddingTop = sBackgroundDrawablePadding.top; mPaddingRight = sBackgroundDrawablePadding.right; mPaddingBottom = sBackgroundDrawablePadding.bottom; } else if (!requiresBackgroundDrawable && mCurrentShadowDrawn) { // turn off background drawing mCurrentShadowDrawn = 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 = mCurrentShadowDrawn && sBackgroundDrawable != null && !isPressed() && !mBackgroundDisabled; 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 && !mBackgroundDisabled; 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. show badge, if requested if (requiresBadge()) { // retrieve the badge resources final BadgeAssets ba = sBadges.get(mTrustLevel); if (ba != null) { // paint back final Drawable back = ba.back; int badgeL = 0, badgeT = 0, badgeW = 0, badgeH = 0; if (back != null) { badgeW = back.getIntrinsicWidth(); badgeH = back.getIntrinsicHeight(); badgeL = mCurrentWidth - mPaddingRight / 3 - badgeW; badgeT = mCurrentHeight - mPaddingBottom / 3 - badgeH; back.setBounds(badgeL, badgeT, badgeL + badgeW, badgeT + badgeH); back.draw(canvas); } int messagesCount = computeBadgeMessages(); // paint accent, if 0 messages if (messagesCount < 1) { final Drawable accent = ba.accent; if (accent != null && badgeW > 0 && badgeH > 0) { int accentW = accent.getIntrinsicWidth(); int accentH = accent.getIntrinsicHeight(); int accentL = badgeL + (badgeW - accentW) / 2; int accentT = badgeT + (badgeH - accentH) / 2; accent.setBounds(accentL, accentT, accentL + accentW, accentT + accentH); accent.draw(canvas); } } // at least 1 message, draw text else if (Color.alpha(ba.textColor) > 0) { float textSize = Math.min(2 * contentWidth / 5, sMediumPxThreshold / 4) * 1.1f; sBadgeTextPaint.setTextSize(textSize); sBadgeTextPaint.setColor(ba.textColor); final String text = String.valueOf(messagesCount); int textWidth = Math.round(sBadgeTextPaint.measureText(text) / 2); int textCx = badgeL + badgeW / 2; int textCy = badgeT + badgeH / 2; canvas.drawText(text, textCx - textWidth, textCy + textSize / 3 + 1, sBadgeTextPaint); } } } /*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) { final int color = bitmap.getPixel(x, y); // discard semi-transparent pixels, because they're probably from a spurious border //if ((color >>> 24) <= 128) // return 0; // compose transparent pixels with white, since the BG will be white anyway final int alpha = Color.alpha(color); if (alpha == 0) return Color.WHITE; if (alpha < 255) { // perform simplified Porter-Duff source-over int dstContribution = 255 - alpha; return Color.argb(255, ((alpha * Color.red(color)) >> 8) + dstContribution, ((alpha * Color.green(color)) >> 8) + dstContribution, ((alpha * Color.blue(color)) >> 8) + dstContribution ); } // discard black pixels, because black is not a color (well, not a good looking one) if ((color & 0xFFFFFF) == 0) return 0; return color; } }