Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (c) 2015, The Linux Foundation. All rights reserved. |
| 3 | * |
| 4 | * Redistribution and use in source and binary forms, with or without |
| 5 | * modification, are permitted provided that the following conditions are |
| 6 | * met: |
| 7 | * * Redistributions of source code must retain the above copyright |
| 8 | * notice, this list of conditions and the following disclaimer. |
| 9 | * * Redistributions in binary form must reproduce the above |
| 10 | * copyright notice, this list of conditions and the following |
| 11 | * disclaimer in the documentation and/or other materials provided |
| 12 | * with the distribution. |
| 13 | * * Neither the name of The Linux Foundation nor the names of its |
| 14 | * contributors may be used to endorse or promote products derived |
| 15 | * from this software without specific prior written permission. |
| 16 | * |
| 17 | * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED |
| 18 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
| 19 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT |
| 20 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS |
| 21 | * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| 22 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| 23 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR |
| 24 | * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, |
| 25 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE |
| 26 | * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN |
| 27 | * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 28 | */ |
| 29 | package com.android.browser; |
| 30 | |
| 31 | import android.content.Context; |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 32 | import android.content.res.Resources; |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 33 | import android.content.res.TypedArray; |
| 34 | import android.graphics.Bitmap; |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 35 | import android.graphics.BitmapFactory; |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 36 | import android.graphics.Canvas; |
| 37 | import android.graphics.Color; |
| 38 | import android.graphics.Paint; |
| 39 | import android.graphics.Rect; |
| 40 | import android.graphics.RectF; |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 41 | import android.graphics.Typeface; |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 42 | import android.graphics.drawable.BitmapDrawable; |
| 43 | import android.graphics.drawable.Drawable; |
| 44 | import android.util.AttributeSet; |
| 45 | import android.util.DisplayMetrics; |
| 46 | import android.util.TypedValue; |
| 47 | import android.view.View; |
| 48 | |
Sagar Dhawan | ca9ecfb | 2015-08-10 17:27:58 -0700 | [diff] [blame] | 49 | import java.util.HashMap; |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 50 | import java.util.Map; |
| 51 | |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 52 | /** |
| 53 | * This represents a WebSite Tile that is created from a Drawable and will scale across any |
| 54 | * area this is externally layouted to. There are 3 possible looks: |
| 55 | * - just the favicon (TYPE_SMALL) |
| 56 | * - drop-shadow plus a thin overlay border (1dp) (TYPE_MEDIUM) |
| 57 | * - centered favicon, extended color, rounded base (TYPE_LARGE) |
| 58 | * |
| 59 | * By centralizing everything in this class we make customization of looks much easier. |
| 60 | * |
| 61 | * NOTES: |
| 62 | * - do not set a background from the outside; this overrides it automatically |
| 63 | */ |
| 64 | public class SiteTileView extends View { |
| 65 | |
Pankaj Garg | 66f8cef | 2015-07-07 17:29:03 -0700 | [diff] [blame] | 66 | // public trust level constants |
| 67 | public static final int TRUST_UNKNOWN = 0; // default |
| 68 | public static final int TRUST_AVOID = 0x01; |
| 69 | public static final int TRUST_UNTRUSTED = 0x02; |
| 70 | public static final int TRUST_TRUSTED = 0x04; |
| 71 | private static final int TRUST_MASK = 0x07; |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 72 | |
| 73 | |
| 74 | // static configuration |
| 75 | private static final int THRESHOLD_MEDIUM_DP = 32; |
| 76 | private static final int THRESHOLD_LARGE_DP = 64; |
| 77 | private static final int LARGE_FAVICON_SIZE_DP = 48; |
| 78 | private static final int BACKGROUND_DRAWABLE_RES = R.drawable.img_tile_background; |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 79 | private static final int DEFAULT_SITE_FAVICON = 0; |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 80 | private static final float FILLER_RADIUS_DP = 2f; // sync with the bg image radius |
| 81 | private static final int FILLER_FALLBACK_COLOR = Color.WHITE; // in case there is no favicon |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 82 | private static final boolean BADGE_SHOW_BLOCKED_COUNT = false; |
Pankaj Garg | 66f8cef | 2015-07-07 17:29:03 -0700 | [diff] [blame] | 83 | |
| 84 | // internal enums |
| 85 | private static final int TYPE_SMALL = 1; |
| 86 | private static final int TYPE_MEDIUM = 2; |
| 87 | private static final int TYPE_LARGE = 3; |
| 88 | private static final int TYPE_AUTO = 0; |
| 89 | private static final int COLOR_AUTO = 0; |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 90 | |
| 91 | |
| 92 | // configuration |
| 93 | private Bitmap mFaviconBitmap = null; |
| 94 | private Paint mFundamentalPaint = null; |
| 95 | private int mFaviconWidth = 0; |
| 96 | private int mFaviconHeight = 0; |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 97 | private int mForcedFundamentalColor = COLOR_AUTO; |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 98 | private boolean mBackgroundDisabled = false; |
Pankaj Garg | 66f8cef | 2015-07-07 17:29:03 -0700 | [diff] [blame] | 99 | private int mTrustLevel = TRUST_UNKNOWN; |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 100 | private int mBadgeBlockedObjectsCount = 0; |
| 101 | private boolean mBadgeHasCertIssues = false; |
Pankaj Garg | 66f8cef | 2015-07-07 17:29:03 -0700 | [diff] [blame] | 102 | |
| 103 | // runtime params set on Layout |
| 104 | private int mCurrentWidth = 0; |
| 105 | private int mCurrentHeight = 0; |
| 106 | private int mCurrentType = TYPE_MEDIUM; |
| 107 | private int mPaddingLeft = 0; |
| 108 | private int mPaddingTop = 0; |
| 109 | private int mPaddingRight = 0; |
| 110 | private int mPaddingBottom = 0; |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 111 | private boolean mCurrentShadowDrawn = false; |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 112 | |
| 113 | // static objects, to be recycled amongst instances (this is an optimization) |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 114 | // NOTE: package-visible statics are for optimized usage inside FolderTileView as well |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 115 | private static int sMediumPxThreshold = -1; |
| 116 | private static int sLargePxThreshold = -1; |
| 117 | private static int sLargeFaviconPx = -1; |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 118 | /* package */ static float sRoundedRadius = -1; |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 119 | private static Paint sBitmapPaint = null; |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 120 | private static Paint sBadgeTextPaint = null; |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 121 | private static Rect sSrcRect = new Rect(); |
| 122 | private static Rect sDstRect = new Rect(); |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 123 | /* package */ static RectF sRectF = new RectF(); |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 124 | private static Drawable sBackgroundDrawable = null; |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 125 | private static class BadgeAssets { |
| 126 | Drawable back; |
| 127 | Drawable accent; |
| 128 | int textColor; |
| 129 | } |
| 130 | private static Map<Integer, BadgeAssets> sBadges; |
| 131 | private static Bitmap sDefaultSiteBitmap = null; |
| 132 | /* package */ static Rect sBackgroundDrawablePadding = new Rect(); |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 133 | |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 134 | |
| 135 | /* XML constructors */ |
| 136 | |
| 137 | public SiteTileView(Context context) { |
| 138 | super(context); |
| 139 | xmlInit(null, 0); |
| 140 | } |
| 141 | |
| 142 | public SiteTileView(Context context, AttributeSet attrs) { |
| 143 | super(context, attrs); |
| 144 | xmlInit(attrs, 0); |
| 145 | } |
| 146 | |
| 147 | public SiteTileView(Context context, AttributeSet attrs, int defStyle) { |
| 148 | super(context, attrs, defStyle); |
| 149 | xmlInit(attrs, defStyle); |
| 150 | } |
| 151 | |
| 152 | |
| 153 | /* Programmatic Constructors */ |
| 154 | |
| 155 | public SiteTileView(Context context, Bitmap favicon) { |
| 156 | super(context); |
| 157 | init(favicon, COLOR_AUTO); |
| 158 | } |
| 159 | |
| 160 | public SiteTileView(Context context, Bitmap favicon, int fundamentalColor) { |
| 161 | super(context); |
| 162 | init(favicon, fundamentalColor); |
| 163 | } |
| 164 | |
| 165 | |
| 166 | /** |
| 167 | * Changes the current favicon (and associated fundamental color) on the fly |
| 168 | */ |
| 169 | public void replaceFavicon(Bitmap favicon) { |
| 170 | replaceFavicon(favicon, COLOR_AUTO); |
| 171 | } |
| 172 | |
| 173 | /** |
| 174 | * Changes the current favicon (and associated fundamental color) on the fly |
| 175 | * @param favicon the new favicon |
| 176 | * @param fundamentalColor the new fudamental color, or COLOR_AUTO |
| 177 | */ |
| 178 | public void replaceFavicon(Bitmap favicon, int fundamentalColor) { |
| 179 | init(favicon, fundamentalColor); |
| 180 | requestLayout(); |
| 181 | } |
| 182 | |
| 183 | /** |
| 184 | * Disables the automatic background and filling. Useful for things that are not really |
| 185 | * "Website Tiles", like folders. |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 186 | * @param disabled true to disable the background (defaults to false) |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 187 | */ |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 188 | public void setBackgroundDisabled(boolean disabled) { |
| 189 | if (mBackgroundDisabled != disabled) { |
| 190 | mBackgroundDisabled = disabled; |
Pankaj Garg | 66f8cef | 2015-07-07 17:29:03 -0700 | [diff] [blame] | 191 | invalidate(); |
| 192 | } |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 193 | } |
| 194 | |
Pankaj Garg | 66f8cef | 2015-07-07 17:29:03 -0700 | [diff] [blame] | 195 | /** |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 196 | * This results in the Badge being updated |
Pankaj Garg | 66f8cef | 2015-07-07 17:29:03 -0700 | [diff] [blame] | 197 | * @param trustLevel one of the TRUST_ constants |
| 198 | */ |
| 199 | public void setTrustLevel(int trustLevel) { |
| 200 | if (mTrustLevel != trustLevel) { |
| 201 | mTrustLevel = trustLevel; |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 202 | invalidate(); |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | /** |
| 207 | * Tells that there will be some message about issues inside |
| 208 | * @param certIssues true if there are issues. |
| 209 | */ |
| 210 | public void setBadgeHasCertIssues(boolean certIssues) { |
| 211 | if (certIssues != mBadgeHasCertIssues) { |
| 212 | mBadgeHasCertIssues = certIssues; |
| 213 | invalidate(); |
Pankaj Garg | 66f8cef | 2015-07-07 17:29:03 -0700 | [diff] [blame] | 214 | } |
| 215 | } |
| 216 | |
| 217 | /** |
| 218 | * Sets the number of objects blocked (a positive contribution to the page). Presentation |
| 219 | * may or may not have the number indication. |
| 220 | * @param sessionCounter Counter of blocked objects. Use 0 to not display anything. |
| 221 | */ |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 222 | public void setBadgeBlockedObjectsCount(int sessionCounter) { |
| 223 | if (sessionCounter != mBadgeBlockedObjectsCount) { |
| 224 | // repaint if going from or to 0, or if showing the ads count |
| 225 | //noinspection PointlessBooleanExpression,ConstantConditions |
| 226 | if (mBadgeBlockedObjectsCount == 0 || sessionCounter == 0 || BADGE_SHOW_BLOCKED_COUNT) |
Pankaj Garg | 66f8cef | 2015-07-07 17:29:03 -0700 | [diff] [blame] | 227 | invalidate(); |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 228 | mBadgeBlockedObjectsCount = sessionCounter; |
Pankaj Garg | 66f8cef | 2015-07-07 17:29:03 -0700 | [diff] [blame] | 229 | } |
| 230 | } |
| 231 | |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 232 | |
| 233 | /** |
| 234 | * @return The fundamental color representing the site. |
| 235 | */ |
| 236 | public int getFundamentalColor() { |
| 237 | if (mForcedFundamentalColor != COLOR_AUTO) |
| 238 | return mForcedFundamentalColor; |
| 239 | if (mFundamentalPaint == null) |
| 240 | mFundamentalPaint = createFundamentalPaint(mFaviconBitmap, COLOR_AUTO); |
| 241 | return mFundamentalPaint.getColor(); |
| 242 | } |
| 243 | |
| 244 | |
| 245 | /*** private stuff ahead ***/ |
| 246 | |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 247 | private boolean requiresBadge() { |
| 248 | return !mBackgroundDisabled && (mTrustLevel != TRUST_UNKNOWN || mBadgeHasCertIssues |
| 249 | || mBadgeBlockedObjectsCount > 0); |
| 250 | } |
| 251 | |
| 252 | private int computeBadgeMessages() { |
| 253 | // special case, for TRUST_AVOID, always show the common accent |
| 254 | if (mTrustLevel == TRUST_AVOID) |
| 255 | return 0; |
| 256 | |
| 257 | // recompute number of 'messages' inside the badge |
| 258 | int count = 0; |
| 259 | if (mBadgeHasCertIssues) |
| 260 | count++; |
| 261 | if (mBadgeBlockedObjectsCount > 0) |
| 262 | count++; |
| 263 | |
| 264 | // add the number of blocked objects (-1, for having already counted the message) if needed |
| 265 | if (BADGE_SHOW_BLOCKED_COUNT) |
| 266 | count += mBadgeBlockedObjectsCount - 1; |
| 267 | |
| 268 | return count; |
| 269 | } |
| 270 | |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 271 | private void xmlInit(AttributeSet attrs, int defStyle) { |
| 272 | // load attributes |
| 273 | final TypedArray a = getContext().obtainStyledAttributes(attrs, |
| 274 | R.styleable.SiteTileView, defStyle, 0); |
| 275 | |
| 276 | // fetch the drawable, if defined - then just extract and use the bitmap |
| 277 | final Drawable drawable = a.getDrawable(R.styleable.SiteTileView_android_src); |
| 278 | final Bitmap favicon = drawable instanceof BitmapDrawable ? |
| 279 | ((BitmapDrawable) drawable).getBitmap() : null; |
| 280 | |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 281 | // check if we want it background-less (disable shadow and filler) |
| 282 | setBackgroundDisabled(a.getBoolean(R.styleable.SiteTileView_disableBackground, false)); |
Pankaj Garg | 66f8cef | 2015-07-07 17:29:03 -0700 | [diff] [blame] | 283 | |
| 284 | // read the trust level (unknown, aka 'default', if not present) |
| 285 | setTrustLevel(a.getInteger(R.styleable.SiteTileView_trustLevel, TRUST_UNKNOWN) |
| 286 | & TRUST_MASK); |
| 287 | |
| 288 | // read the amount of blocked objects (or 0 if not present) |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 289 | setBadgeBlockedObjectsCount(a.getInteger(R.styleable.SiteTileView_blockedObjects, 0)); |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 290 | |
| 291 | // delete attribute resolution |
| 292 | a.recycle(); |
| 293 | |
| 294 | // proceed with real initialization |
| 295 | init(favicon, COLOR_AUTO); |
| 296 | } |
| 297 | |
| 298 | private void init(Bitmap favicon, int fundamentalColor) { |
| 299 | mFaviconBitmap = favicon; |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 300 | |
| 301 | // show a default favicon if nothing is set (consider removing this, it's ugly) |
| 302 | if (mFaviconBitmap == null && DEFAULT_SITE_FAVICON != 0) { |
| 303 | if (sDefaultSiteBitmap == null) |
| 304 | sDefaultSiteBitmap = BitmapFactory.decodeResource(getResources(), |
| 305 | DEFAULT_SITE_FAVICON); |
| 306 | mFaviconBitmap = sDefaultSiteBitmap; |
| 307 | fundamentalColor = 0xFF262626; |
| 308 | } |
| 309 | |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 310 | if (mFaviconBitmap != null) { |
| 311 | mFaviconWidth = mFaviconBitmap.getWidth(); |
| 312 | mFaviconHeight = mFaviconBitmap.getHeight(); |
| 313 | } |
| 314 | |
| 315 | // don't compute the paint right now, just save any hint for later |
| 316 | mFundamentalPaint = null; |
| 317 | mForcedFundamentalColor = fundamentalColor; |
| 318 | |
| 319 | // shared (static) resources initialization; except for background, inited on-demand |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 320 | ensureCommonLoaded(getResources()); |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 321 | |
| 322 | // change when clicked |
| 323 | setClickable(true); |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 324 | } |
| 325 | |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 326 | static void ensureCommonLoaded(Resources r) { |
| 327 | // check if already initialized |
| 328 | if (sMediumPxThreshold != -1) |
| 329 | return; |
| 330 | |
| 331 | // heuristics thresholds |
| 332 | final DisplayMetrics displayMetrics = r.getDisplayMetrics(); |
| 333 | sMediumPxThreshold = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, |
| 334 | THRESHOLD_MEDIUM_DP, displayMetrics); |
| 335 | sLargePxThreshold = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, |
| 336 | THRESHOLD_LARGE_DP, displayMetrics); |
| 337 | sLargeFaviconPx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, |
| 338 | LARGE_FAVICON_SIZE_DP, displayMetrics); |
| 339 | |
| 340 | // rounded radius |
| 341 | sRoundedRadius = FILLER_RADIUS_DP > 0 ? TypedValue.applyDimension( |
| 342 | TypedValue.COMPLEX_UNIT_DIP, FILLER_RADIUS_DP, displayMetrics) : 0; |
| 343 | |
| 344 | // bitmap paint (copy, smooth scale) |
| 345 | sBitmapPaint = new Paint(); |
| 346 | sBitmapPaint.setColor(Color.BLACK); |
| 347 | sBitmapPaint.setFilterBitmap(true); |
| 348 | |
| 349 | // badge text paint (anti-aliased) |
| 350 | sBadgeTextPaint = new Paint(); |
| 351 | sBadgeTextPaint.setAntiAlias(true); |
| 352 | Typeface badgeTypeface = Typeface.create("sans-serif-medium", Typeface.NORMAL); |
| 353 | if (badgeTypeface != null) |
| 354 | sBadgeTextPaint.setTypeface(badgeTypeface); |
| 355 | |
| 356 | // load the background (could be loaded on demand, but in the end it's always needed) |
| 357 | sBackgroundDrawable = r.getDrawable(BACKGROUND_DRAWABLE_RES); |
| 358 | if (sBackgroundDrawable != null) |
| 359 | sBackgroundDrawable.getPadding(sBackgroundDrawablePadding); |
| 360 | |
| 361 | // load all the badge drawables |
Sagar Dhawan | ca9ecfb | 2015-08-10 17:27:58 -0700 | [diff] [blame] | 362 | sBadges = new HashMap<>(); |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 363 | loadBadgeResources(r, TRUST_AVOID, R.drawable.img_deco_tile_avoid, |
| 364 | R.drawable.img_deco_tile_avoid_accent, R.color.TileBadgeTextAvoid); |
| 365 | loadBadgeResources(r, TRUST_UNTRUSTED, R.drawable.img_deco_tile_untrusted, |
| 366 | R.drawable.img_deco_tile_untrusted_accent, R.color.TileBadgeTextUntrusted); |
| 367 | loadBadgeResources(r, TRUST_UNKNOWN, R.drawable.img_deco_tile_unknown, |
| 368 | R.drawable.img_deco_tile_unknown_accent, R.color.TileBadgeTextUnknown); |
| 369 | loadBadgeResources(r, TRUST_TRUSTED, R.drawable.img_deco_tile_verified, |
| 370 | R.drawable.img_deco_tile_verified_accent, R.color.TileBadgeTextVerified); |
| 371 | } |
| 372 | |
| 373 | private static void loadBadgeResources(Resources r, int t, int back, int accent, int color) { |
| 374 | BadgeAssets ba = new BadgeAssets(); |
| 375 | ba.back = back == 0 ? null : r.getDrawable(back); |
| 376 | ba.accent = accent == 0 ? null : r.getDrawable(accent); |
| 377 | ba.textColor = color == 0 ? Color.TRANSPARENT : r.getColor(color); |
| 378 | sBadges.put(t, ba); |
| 379 | } |
| 380 | |
| 381 | static Rect getBackgroundDrawablePadding() { |
| 382 | return sBackgroundDrawablePadding != null ? sBackgroundDrawablePadding : new Rect(); |
| 383 | } |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 384 | |
| 385 | @Override |
| 386 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { |
| 387 | mCurrentWidth = right - left; |
| 388 | mCurrentHeight = bottom - top; |
| 389 | |
| 390 | // auto-determine the "TYPE_" from the physical size of the layout |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 391 | if (mCurrentWidth < sMediumPxThreshold && mCurrentHeight < sMediumPxThreshold) |
| 392 | mCurrentType = TYPE_SMALL; |
| 393 | else if (mCurrentWidth < sLargePxThreshold && mCurrentHeight < sLargePxThreshold) |
| 394 | mCurrentType = TYPE_MEDIUM; |
| 395 | else |
| 396 | mCurrentType = TYPE_LARGE; |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 397 | |
| 398 | // set or remove the background (if the need changed!) |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 399 | boolean requiresBackgroundDrawable = mCurrentType >= TYPE_MEDIUM; |
| 400 | if (requiresBackgroundDrawable && !mCurrentShadowDrawn) { |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 401 | // draw the background |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 402 | mCurrentShadowDrawn = mCurrentType >= TYPE_LARGE; |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 403 | |
| 404 | // background -> padding |
| 405 | mPaddingLeft = sBackgroundDrawablePadding.left; |
| 406 | mPaddingTop = sBackgroundDrawablePadding.top; |
| 407 | mPaddingRight = sBackgroundDrawablePadding.right; |
| 408 | mPaddingBottom = sBackgroundDrawablePadding.bottom; |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 409 | } else if (!requiresBackgroundDrawable && mCurrentShadowDrawn) { |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 410 | // turn off background drawing |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 411 | mCurrentShadowDrawn = false; |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 412 | |
| 413 | // no background -> no padding |
| 414 | mPaddingLeft = 0; |
| 415 | mPaddingTop = 0; |
| 416 | mPaddingRight = 0; |
| 417 | mPaddingBottom = 0; |
| 418 | } |
| 419 | |
| 420 | // just proceed, do nothing here |
| 421 | super.onLayout(changed, left, top, right, bottom); |
| 422 | } |
| 423 | |
| 424 | @Override |
| 425 | public void setPressed(boolean pressed) { |
| 426 | super.setPressed(pressed); |
| 427 | // schedule a repaint to show pressed/released |
| 428 | invalidate(); |
| 429 | } |
| 430 | |
| 431 | @Override |
| 432 | public void setSelected(boolean selected) { |
| 433 | super.setSelected(selected); |
| 434 | // schedule a repaint to show selected |
| 435 | invalidate(); |
| 436 | } |
| 437 | |
| 438 | @Override |
| 439 | protected void onDraw(Canvas canvas) { |
| 440 | super.onDraw(canvas); |
| 441 | |
| 442 | // Selection State: make everything smaller |
| 443 | if (isSelected()) { |
| 444 | float scale = 0.8f; |
| 445 | canvas.translate(mCurrentWidth * (1 - scale) / 2, mCurrentHeight * (1 - scale) / 2); |
| 446 | canvas.scale(scale, scale); |
| 447 | } |
| 448 | |
| 449 | // Pressed state: make the button reach the finger |
| 450 | if (isPressed()) { |
| 451 | float scale = 1.1f; |
| 452 | canvas.translate(mCurrentWidth * (1 - scale) / 2, mCurrentHeight * (1 - scale) / 2); |
| 453 | canvas.scale(scale, scale); |
| 454 | } |
| 455 | |
| 456 | final int left = mPaddingLeft; |
| 457 | final int top = mPaddingTop; |
| 458 | final int right = mCurrentWidth - mPaddingRight; |
| 459 | final int bottom = mCurrentHeight - mPaddingBottom; |
| 460 | final int contentWidth = right - left; |
| 461 | final int contentHeight = bottom - top; |
| 462 | |
| 463 | // A. the background drawable (if set) |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 464 | boolean requiresBackground = mCurrentShadowDrawn && sBackgroundDrawable != null |
| 465 | && !isPressed() && !mBackgroundDisabled; |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 466 | if (requiresBackground) { |
| 467 | sBackgroundDrawable.setBounds(0, 0, mCurrentWidth, mCurrentHeight); |
| 468 | sBackgroundDrawable.draw(canvas); |
| 469 | } |
| 470 | |
| 471 | // B. (when needed) draw the background rectangle; sharp our rounded |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 472 | boolean requiresFundamentalFiller = mCurrentType >= TYPE_LARGE && !mBackgroundDisabled; |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 473 | if (requiresFundamentalFiller) { |
| 474 | // create the filler paint on demand (not all icons need it) |
| 475 | if (mFundamentalPaint == null) |
| 476 | mFundamentalPaint = createFundamentalPaint(mFaviconBitmap, mForcedFundamentalColor); |
| 477 | |
| 478 | // paint if not white, since requiresBackground already painted it white |
| 479 | int fundamentalColor = mFundamentalPaint.getColor(); |
| 480 | if (fundamentalColor != COLOR_AUTO && |
| 481 | (fundamentalColor != Color.WHITE || !requiresBackground)) { |
| 482 | if (sRoundedRadius >= 1.) { |
| 483 | sRectF.set(left, top, right, bottom); |
| 484 | canvas.drawRoundRect(sRectF, sRoundedRadius, sRoundedRadius, mFundamentalPaint); |
| 485 | } else |
| 486 | canvas.drawRect(left, top, right, bottom, mFundamentalPaint); |
| 487 | } |
| 488 | } |
| 489 | |
| 490 | // C. (if present) draw the favicon |
| 491 | boolean requiresFavicon = mFaviconBitmap != null |
| 492 | && mFaviconWidth > 1 && mFaviconHeight > 1; |
| 493 | if (requiresFavicon) { |
| 494 | // destination can either fill, or auto-center |
| 495 | boolean fillSpace = mCurrentType <= TYPE_MEDIUM; |
| 496 | if (fillSpace || contentWidth < sLargeFaviconPx || contentHeight < sLargeFaviconPx) { |
| 497 | sDstRect.set(left, top, right, bottom); |
| 498 | } else { |
| 499 | int dstLeft = left + (contentWidth - sLargeFaviconPx) / 2; |
| 500 | int dstTop = top + (contentHeight - sLargeFaviconPx) / 2; |
| 501 | sDstRect.set(dstLeft, dstTop, dstLeft + sLargeFaviconPx, dstTop + sLargeFaviconPx); |
| 502 | } |
| 503 | |
| 504 | // source has to 'crop proportionally' to keep the dest aspect ratio |
| 505 | sSrcRect.set(0, 0, mFaviconWidth, mFaviconHeight); |
| 506 | int sW = sSrcRect.width(); |
| 507 | int sH = sSrcRect.height(); |
| 508 | int dW = sDstRect.width(); |
| 509 | int dH = sDstRect.height(); |
| 510 | if (sW > 4 && sH > 4 && dW > 4 && dH > 4) { |
| 511 | float hScale = (float) dW / (float) sW; |
| 512 | float vScale = (float) dH / (float) sH; |
| 513 | if (hScale == vScale) { |
| 514 | // no transformation needed, just zoom |
| 515 | } else if (hScale < vScale) { |
| 516 | // horizontal crop |
| 517 | float hCrop = 1 - hScale / vScale; |
| 518 | int hCropPx = (int) (sW * hCrop / 2 + 0.5); |
| 519 | sSrcRect.left += hCropPx; |
| 520 | sSrcRect.right -= hCropPx; |
| 521 | canvas.drawBitmap(mFaviconBitmap, sSrcRect, sDstRect, sBitmapPaint); |
| 522 | } else { |
| 523 | // vertical crop |
| 524 | float vCrop = 1 - vScale / hScale; |
| 525 | int vCropPx = (int) (sH * vCrop / 2 + 0.5); |
| 526 | sSrcRect.top += vCropPx; |
| 527 | sSrcRect.bottom -= vCropPx; |
| 528 | } |
| 529 | } |
| 530 | |
| 531 | // blit favicon, croppped, scaled |
| 532 | canvas.drawBitmap(mFaviconBitmap, sSrcRect, sDstRect, sBitmapPaint); |
| 533 | } |
| 534 | |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 535 | // D. show badge, if requested |
| 536 | if (requiresBadge()) { |
| 537 | // retrieve the badge resources |
| 538 | final BadgeAssets ba = sBadges.get(mTrustLevel); |
| 539 | if (ba != null) { |
| 540 | |
| 541 | // paint back |
| 542 | final Drawable back = ba.back; |
| 543 | int badgeL = 0, badgeT = 0, badgeW = 0, badgeH = 0; |
| 544 | if (back != null) { |
| 545 | badgeW = back.getIntrinsicWidth(); |
| 546 | badgeH = back.getIntrinsicHeight(); |
| 547 | badgeL = mCurrentWidth - mPaddingRight / 3 - badgeW; |
| 548 | badgeT = mCurrentHeight - mPaddingBottom / 3 - badgeH; |
| 549 | back.setBounds(badgeL, badgeT, badgeL + badgeW, badgeT + badgeH); |
| 550 | back.draw(canvas); |
Pankaj Garg | 66f8cef | 2015-07-07 17:29:03 -0700 | [diff] [blame] | 551 | } |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 552 | int messagesCount = computeBadgeMessages(); |
Pankaj Garg | 66f8cef | 2015-07-07 17:29:03 -0700 | [diff] [blame] | 553 | |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 554 | // paint accent, if 0 messages |
| 555 | if (messagesCount < 1) { |
| 556 | final Drawable accent = ba.accent; |
| 557 | if (accent != null && badgeW > 0 && badgeH > 0) { |
| 558 | int accentW = accent.getIntrinsicWidth(); |
| 559 | int accentH = accent.getIntrinsicHeight(); |
| 560 | int accentL = badgeL + (badgeW - accentW) / 2; |
| 561 | int accentT = badgeT + (badgeH - accentH) / 2; |
| 562 | accent.setBounds(accentL, accentT, accentL + accentW, accentT + accentH); |
| 563 | accent.draw(canvas); |
| 564 | } |
| 565 | } |
| 566 | // at least 1 message, draw text |
| 567 | else if (Color.alpha(ba.textColor) > 0) { |
| 568 | float textSize = Math.min(2 * contentWidth / 5, sMediumPxThreshold / 4) * 1.1f; |
| 569 | sBadgeTextPaint.setTextSize(textSize); |
| 570 | sBadgeTextPaint.setColor(ba.textColor); |
| 571 | final String text = String.valueOf(messagesCount); |
| 572 | int textWidth = Math.round(sBadgeTextPaint.measureText(text) / 2); |
| 573 | int textCx = badgeL + badgeW / 2; |
| 574 | int textCy = badgeT + badgeH / 2; |
| 575 | canvas.drawText(text, textCx - textWidth, textCy + textSize / 3 + 1, |
| 576 | sBadgeTextPaint); |
| 577 | } |
Pankaj Garg | 66f8cef | 2015-07-07 17:29:03 -0700 | [diff] [blame] | 578 | } |
| 579 | } |
| 580 | |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 581 | /*if (true) { // DEBUG TYPE |
| 582 | Paint paint = new Paint(); |
| 583 | paint.setColor(Color.BLACK); |
| 584 | paint.setTextSize(20); |
| 585 | canvas.drawText(String.valueOf(mCurrentType), 30, 30, paint); |
| 586 | }*/ |
| 587 | } |
| 588 | |
| 589 | |
| 590 | /** |
| 591 | * Creates a fill Paint from the favicon, or using the forced color (if not COLOR_AUTO) |
| 592 | */ |
| 593 | private static Paint createFundamentalPaint(Bitmap favicon, int forceFillColor) { |
| 594 | final Paint fillPaint = new Paint(); |
| 595 | if (forceFillColor != COLOR_AUTO) |
| 596 | fillPaint.setColor(forceFillColor); |
| 597 | else |
| 598 | fillPaint.setColor(guessFundamentalColor(favicon)); |
| 599 | return fillPaint; |
| 600 | } |
| 601 | |
| 602 | /** |
| 603 | * This uses very stupid mechanism - a 9x9 grid sample on the borders and center - and selects |
| 604 | * the color with the most frequency, or the center. |
| 605 | * |
| 606 | * @param bitmap the bitmap to guesss the color about |
| 607 | * @return a Color |
| 608 | */ |
| 609 | private static int guessFundamentalColor(Bitmap bitmap) { |
| 610 | if (bitmap == null) |
| 611 | return FILLER_FALLBACK_COLOR; |
| 612 | int height = bitmap.getHeight(); |
| 613 | int width = bitmap.getWidth(); |
| 614 | if (height < 2 || width < 2) |
| 615 | return FILLER_FALLBACK_COLOR; |
| 616 | |
| 617 | // pick up to 9 colors |
| 618 | // NOTE: the order of sampling sets the precendece, in case of ties |
| 619 | int[] pxColors = new int[9]; |
| 620 | int idx = 0; |
| 621 | if ((pxColors[idx] = sampleColor(bitmap, width / 2, height / 2)) != 0) idx++; |
| 622 | if ((pxColors[idx] = sampleColor(bitmap, width / 2, height - 1)) != 0) idx++; |
| 623 | if ((pxColors[idx] = sampleColor(bitmap, width - 1, height - 1)) != 0) idx++; |
| 624 | if ((pxColors[idx] = sampleColor(bitmap, width - 1, height / 2)) != 0) idx++; |
| 625 | if ((pxColors[idx] = sampleColor(bitmap, 0, 0 )) != 0) idx++; |
| 626 | if ((pxColors[idx] = sampleColor(bitmap, width / 2, 0 )) != 0) idx++; |
| 627 | if ((pxColors[idx] = sampleColor(bitmap, width - 1, 0 )) != 0) idx++; |
| 628 | if ((pxColors[idx] = sampleColor(bitmap, 0 , height / 2)) != 0) idx++; |
| 629 | if ((pxColors[idx] = sampleColor(bitmap, 0 , height - 1)) != 0) idx++; |
| 630 | |
| 631 | // find the most popular |
| 632 | int popColor = -1; |
| 633 | int popCount = -1; |
| 634 | for (int i = 0; i < idx; i++) { |
| 635 | int thisColor = pxColors[i]; |
| 636 | int thisCount = 0; |
| 637 | for (int j = 0; j < idx; j++) { |
| 638 | if (pxColors[j] == thisColor) |
| 639 | thisCount++; |
| 640 | } |
| 641 | if (thisCount > popCount) { |
| 642 | popColor = thisColor; |
| 643 | popCount = thisCount; |
| 644 | } |
| 645 | } |
| 646 | return popCount > -1 ? popColor : FILLER_FALLBACK_COLOR; |
| 647 | } |
| 648 | |
| 649 | /** |
| 650 | * @return Color, but if it's 0, you should discard it (not representative) |
| 651 | */ |
| 652 | private static int sampleColor(Bitmap bitmap, int x, int y) { |
Pankaj Garg | 66f8cef | 2015-07-07 17:29:03 -0700 | [diff] [blame] | 653 | final int color = bitmap.getPixel(x, y); |
| 654 | |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 655 | // discard semi-transparent pixels, because they're probably from a spurious border |
Pankaj Garg | 66f8cef | 2015-07-07 17:29:03 -0700 | [diff] [blame] | 656 | //if ((color >>> 24) <= 128) |
| 657 | // return 0; |
| 658 | |
| 659 | // compose transparent pixels with white, since the BG will be white anyway |
| 660 | final int alpha = Color.alpha(color); |
| 661 | if (alpha == 0) |
| 662 | return Color.WHITE; |
| 663 | if (alpha < 255) { |
| 664 | // perform simplified Porter-Duff source-over |
| 665 | int dstContribution = 255 - alpha; |
| 666 | return Color.argb(255, |
| 667 | ((alpha * Color.red(color)) >> 8) + dstContribution, |
| 668 | ((alpha * Color.green(color)) >> 8) + dstContribution, |
| 669 | ((alpha * Color.blue(color)) >> 8) + dstContribution |
| 670 | ); |
| 671 | } |
| 672 | |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 673 | // discard black pixels, because black is not a color (well, not a good looking one) |
Pankaj Garg | 66f8cef | 2015-07-07 17:29:03 -0700 | [diff] [blame] | 674 | if ((color & 0xFFFFFF) == 0) |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 675 | return 0; |
Pankaj Garg | 66f8cef | 2015-07-07 17:29:03 -0700 | [diff] [blame] | 676 | |
Pankaj Garg | 21dad56 | 2015-07-02 17:17:24 -0700 | [diff] [blame] | 677 | return color; |
| 678 | } |
| 679 | |
Pankaj Garg | 07b2fd9 | 2015-07-15 12:07:37 -0700 | [diff] [blame] | 680 | } |