1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.launcher3.icons; 17 18 import static com.android.launcher3.icons.ThemedIconDrawable.getColors; 19 20 import android.annotation.TargetApi; 21 import android.content.Context; 22 import android.content.pm.ApplicationInfo; 23 import android.content.pm.PackageManager; 24 import android.content.res.Resources; 25 import android.content.res.TypedArray; 26 import android.graphics.Bitmap; 27 import android.graphics.Canvas; 28 import android.graphics.ColorFilter; 29 import android.graphics.Paint; 30 import android.graphics.PorterDuff.Mode; 31 import android.graphics.PorterDuffColorFilter; 32 import android.graphics.Rect; 33 import android.graphics.drawable.AdaptiveIconDrawable; 34 import android.graphics.drawable.ColorDrawable; 35 import android.graphics.drawable.Drawable; 36 import android.graphics.drawable.LayerDrawable; 37 import android.os.Build; 38 import android.os.Bundle; 39 import android.os.Process; 40 import android.os.SystemClock; 41 import android.os.UserHandle; 42 import android.util.Log; 43 import android.util.TypedValue; 44 45 import androidx.annotation.Nullable; 46 47 import com.android.launcher3.icons.ThemedIconDrawable.ThemeData; 48 49 import java.util.Calendar; 50 import java.util.concurrent.TimeUnit; 51 import java.util.function.IntFunction; 52 53 /** 54 * Wrapper over {@link AdaptiveIconDrawable} to intercept icon flattening logic for dynamic 55 * clock icons 56 */ 57 @TargetApi(Build.VERSION_CODES.O) 58 public class ClockDrawableWrapper extends AdaptiveIconDrawable implements BitmapInfo.Extender { 59 60 private static final String TAG = "ClockDrawableWrapper"; 61 62 private static final boolean DISABLE_SECONDS = true; 63 64 // Time after which the clock icon should check for an update. The actual invalidate 65 // will only happen in case of any change. 66 public static final long TICK_MS = DISABLE_SECONDS ? TimeUnit.MINUTES.toMillis(1) : 200L; 67 68 private static final String LAUNCHER_PACKAGE = "com.android.launcher3"; 69 private static final String ROUND_ICON_METADATA_KEY = LAUNCHER_PACKAGE 70 + ".LEVEL_PER_TICK_ICON_ROUND"; 71 private static final String HOUR_INDEX_METADATA_KEY = LAUNCHER_PACKAGE + ".HOUR_LAYER_INDEX"; 72 private static final String MINUTE_INDEX_METADATA_KEY = LAUNCHER_PACKAGE 73 + ".MINUTE_LAYER_INDEX"; 74 private static final String SECOND_INDEX_METADATA_KEY = LAUNCHER_PACKAGE 75 + ".SECOND_LAYER_INDEX"; 76 private static final String DEFAULT_HOUR_METADATA_KEY = LAUNCHER_PACKAGE 77 + ".DEFAULT_HOUR"; 78 private static final String DEFAULT_MINUTE_METADATA_KEY = LAUNCHER_PACKAGE 79 + ".DEFAULT_MINUTE"; 80 private static final String DEFAULT_SECOND_METADATA_KEY = LAUNCHER_PACKAGE 81 + ".DEFAULT_SECOND"; 82 83 /* Number of levels to jump per second for the second hand */ 84 private static final int LEVELS_PER_SECOND = 10; 85 86 public static final int INVALID_VALUE = -1; 87 88 private final AnimationInfo mAnimationInfo = new AnimationInfo(); 89 private int mTargetSdkVersion; 90 protected ThemeData mThemeData; 91 ClockDrawableWrapper(AdaptiveIconDrawable base)92 public ClockDrawableWrapper(AdaptiveIconDrawable base) { 93 super(base.getBackground(), base.getForeground()); 94 } 95 96 /** 97 * Loads and returns the wrapper from the provided package, or returns null 98 * if it is unable to load. 99 */ forPackage(Context context, String pkg, int iconDpi)100 public static ClockDrawableWrapper forPackage(Context context, String pkg, int iconDpi) { 101 try { 102 PackageManager pm = context.getPackageManager(); 103 ApplicationInfo appInfo = pm.getApplicationInfo(pkg, 104 PackageManager.MATCH_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA); 105 Resources res = pm.getResourcesForApplication(appInfo); 106 return forExtras(appInfo, appInfo.metaData, 107 resId -> res.getDrawableForDensity(resId, iconDpi)); 108 } catch (Exception e) { 109 Log.d(TAG, "Unable to load clock drawable info", e); 110 } 111 return null; 112 } 113 fromThemeData(Context context, ThemeData themeData)114 private static ClockDrawableWrapper fromThemeData(Context context, ThemeData themeData) { 115 try { 116 TypedArray ta = themeData.mResources.obtainTypedArray(themeData.mResID); 117 int count = ta.length(); 118 Bundle extras = new Bundle(); 119 for (int i = 0; i < count; i += 2) { 120 TypedValue v = ta.peekValue(i + 1); 121 extras.putInt(ta.getString(i), v.type >= TypedValue.TYPE_FIRST_INT 122 && v.type <= TypedValue.TYPE_LAST_INT 123 ? v.data : v.resourceId); 124 } 125 ta.recycle(); 126 ClockDrawableWrapper drawable = ClockDrawableWrapper.forExtras( 127 context.getApplicationInfo(), extras, resId -> { 128 int[] colors = getColors(context); 129 Drawable bg = new ColorDrawable(colors[0]); 130 Drawable fg = themeData.mResources.getDrawable(resId).mutate(); 131 fg.setTint(colors[1]); 132 return new AdaptiveIconDrawable(bg, fg); 133 }); 134 if (drawable != null) { 135 return drawable; 136 } 137 } catch (Exception e) { 138 Log.e(TAG, "Error loading themed clock", e); 139 } 140 return null; 141 } 142 forExtras(ApplicationInfo appInfo, Bundle metadata, IntFunction<Drawable> drawableProvider)143 private static ClockDrawableWrapper forExtras(ApplicationInfo appInfo, Bundle metadata, 144 IntFunction<Drawable> drawableProvider) { 145 if (metadata == null) { 146 return null; 147 } 148 int drawableId = metadata.getInt(ROUND_ICON_METADATA_KEY, 0); 149 if (drawableId == 0) { 150 return null; 151 } 152 153 Drawable drawable = drawableProvider.apply(drawableId).mutate(); 154 if (!(drawable instanceof AdaptiveIconDrawable)) { 155 return null; 156 } 157 158 ClockDrawableWrapper wrapper = 159 new ClockDrawableWrapper((AdaptiveIconDrawable) drawable); 160 wrapper.mTargetSdkVersion = appInfo.targetSdkVersion; 161 AnimationInfo info = wrapper.mAnimationInfo; 162 163 info.baseDrawableState = drawable.getConstantState(); 164 165 info.hourLayerIndex = metadata.getInt(HOUR_INDEX_METADATA_KEY, INVALID_VALUE); 166 info.minuteLayerIndex = metadata.getInt(MINUTE_INDEX_METADATA_KEY, INVALID_VALUE); 167 info.secondLayerIndex = metadata.getInt(SECOND_INDEX_METADATA_KEY, INVALID_VALUE); 168 169 info.defaultHour = metadata.getInt(DEFAULT_HOUR_METADATA_KEY, 0); 170 info.defaultMinute = metadata.getInt(DEFAULT_MINUTE_METADATA_KEY, 0); 171 info.defaultSecond = metadata.getInt(DEFAULT_SECOND_METADATA_KEY, 0); 172 173 LayerDrawable foreground = (LayerDrawable) wrapper.getForeground(); 174 int layerCount = foreground.getNumberOfLayers(); 175 if (info.hourLayerIndex < 0 || info.hourLayerIndex >= layerCount) { 176 info.hourLayerIndex = INVALID_VALUE; 177 } 178 if (info.minuteLayerIndex < 0 || info.minuteLayerIndex >= layerCount) { 179 info.minuteLayerIndex = INVALID_VALUE; 180 } 181 if (info.secondLayerIndex < 0 || info.secondLayerIndex >= layerCount) { 182 info.secondLayerIndex = INVALID_VALUE; 183 } else if (DISABLE_SECONDS) { 184 foreground.setDrawable(info.secondLayerIndex, null); 185 info.secondLayerIndex = INVALID_VALUE; 186 } 187 info.applyTime(Calendar.getInstance(), foreground); 188 return wrapper; 189 } 190 191 @Override getExtendedInfo(Bitmap bitmap, int color, BaseIconFactory iconFactory, float normalizationScale, UserHandle user)192 public ClockBitmapInfo getExtendedInfo(Bitmap bitmap, int color, 193 BaseIconFactory iconFactory, float normalizationScale, UserHandle user) { 194 iconFactory.disableColorExtraction(); 195 AdaptiveIconDrawable background = new AdaptiveIconDrawable( 196 getBackground().getConstantState().newDrawable(), null); 197 BitmapInfo bitmapInfo = iconFactory.createBadgedIconBitmap(background, 198 Process.myUserHandle(), mTargetSdkVersion, false); 199 200 return new ClockBitmapInfo(bitmap, color, normalizationScale, 201 mAnimationInfo, bitmapInfo.icon, mThemeData); 202 } 203 204 @Override drawForPersistence(Canvas canvas)205 public void drawForPersistence(Canvas canvas) { 206 LayerDrawable foreground = (LayerDrawable) getForeground(); 207 resetLevel(foreground, mAnimationInfo.hourLayerIndex); 208 resetLevel(foreground, mAnimationInfo.minuteLayerIndex); 209 resetLevel(foreground, mAnimationInfo.secondLayerIndex); 210 draw(canvas); 211 mAnimationInfo.applyTime(Calendar.getInstance(), (LayerDrawable) getForeground()); 212 } 213 214 @Override getThemedDrawable(Context context)215 public Drawable getThemedDrawable(Context context) { 216 if (mThemeData != null) { 217 ClockDrawableWrapper drawable = fromThemeData(context, mThemeData); 218 return drawable == null ? this : drawable; 219 } 220 return this; 221 } 222 resetLevel(LayerDrawable drawable, int index)223 private void resetLevel(LayerDrawable drawable, int index) { 224 if (index != INVALID_VALUE) { 225 drawable.getDrawable(index).setLevel(0); 226 } 227 } 228 229 private static class AnimationInfo { 230 231 public ConstantState baseDrawableState; 232 233 public int hourLayerIndex; 234 public int minuteLayerIndex; 235 public int secondLayerIndex; 236 public int defaultHour; 237 public int defaultMinute; 238 public int defaultSecond; 239 applyTime(Calendar time, LayerDrawable foregroundDrawable)240 boolean applyTime(Calendar time, LayerDrawable foregroundDrawable) { 241 time.setTimeInMillis(System.currentTimeMillis()); 242 243 // We need to rotate by the difference from the default time if one is specified. 244 int convertedHour = (time.get(Calendar.HOUR) + (12 - defaultHour)) % 12; 245 int convertedMinute = (time.get(Calendar.MINUTE) + (60 - defaultMinute)) % 60; 246 int convertedSecond = (time.get(Calendar.SECOND) + (60 - defaultSecond)) % 60; 247 248 boolean invalidate = false; 249 if (hourLayerIndex != INVALID_VALUE) { 250 final Drawable hour = foregroundDrawable.getDrawable(hourLayerIndex); 251 if (hour.setLevel(convertedHour * 60 + time.get(Calendar.MINUTE))) { 252 invalidate = true; 253 } 254 } 255 256 if (minuteLayerIndex != INVALID_VALUE) { 257 final Drawable minute = foregroundDrawable.getDrawable(minuteLayerIndex); 258 if (minute.setLevel(time.get(Calendar.HOUR) * 60 + convertedMinute)) { 259 invalidate = true; 260 } 261 } 262 263 if (secondLayerIndex != INVALID_VALUE) { 264 final Drawable second = foregroundDrawable.getDrawable(secondLayerIndex); 265 if (second.setLevel(convertedSecond * LEVELS_PER_SECOND)) { 266 invalidate = true; 267 } 268 } 269 270 return invalidate; 271 } 272 } 273 274 static class ClockBitmapInfo extends BitmapInfo { 275 276 public final float scale; 277 public final int offset; 278 public final AnimationInfo animInfo; 279 public final Bitmap mFlattenedBackground; 280 281 public final ThemeData themeData; 282 public final ColorFilter bgFilter; 283 ClockBitmapInfo(Bitmap icon, int color, float scale, AnimationInfo animInfo, Bitmap background, ThemeData themeData)284 ClockBitmapInfo(Bitmap icon, int color, float scale, AnimationInfo animInfo, 285 Bitmap background, ThemeData themeData) { 286 this(icon, color, scale, animInfo, background, themeData, null); 287 } 288 ClockBitmapInfo(Bitmap icon, int color, float scale, AnimationInfo animInfo, Bitmap background, ThemeData themeData, ColorFilter bgFilter)289 ClockBitmapInfo(Bitmap icon, int color, float scale, AnimationInfo animInfo, 290 Bitmap background, ThemeData themeData, ColorFilter bgFilter) { 291 super(icon, color); 292 this.scale = scale; 293 this.animInfo = animInfo; 294 this.offset = (int) Math.ceil(ShadowGenerator.BLUR_FACTOR * icon.getWidth()); 295 this.mFlattenedBackground = background; 296 this.themeData = themeData; 297 this.bgFilter = bgFilter; 298 } 299 300 @Override newThemedIcon(Context context)301 public FastBitmapDrawable newThemedIcon(Context context) { 302 if (themeData != null) { 303 ClockDrawableWrapper wrapper = fromThemeData(context, themeData); 304 if (wrapper != null) { 305 int[] colors = getColors(context); 306 ColorFilter bgFilter = new PorterDuffColorFilter(colors[0], Mode.SRC_ATOP); 307 return new ClockBitmapInfo(icon, colors[1], scale, 308 wrapper.mAnimationInfo, mFlattenedBackground, themeData, bgFilter) 309 .newIcon(context); 310 } 311 } 312 return super.newThemedIcon(context); 313 } 314 315 @Override newIcon(Context context)316 public FastBitmapDrawable newIcon(Context context) { 317 ClockIconDrawable d = new ClockIconDrawable(this); 318 d.mDisabledAlpha = GraphicsUtils.getFloat(context, R.attr.disabledIconAlpha, 1f); 319 return d; 320 } 321 322 @Nullable 323 @Override toByteArray()324 public byte[] toByteArray() { 325 return null; 326 } 327 drawBackground(Canvas canvas, Rect bounds, Paint paint)328 void drawBackground(Canvas canvas, Rect bounds, Paint paint) { 329 // draw the background that is already flattened to a bitmap 330 ColorFilter oldFilter = paint.getColorFilter(); 331 if (bgFilter != null) { 332 paint.setColorFilter(bgFilter); 333 } 334 canvas.drawBitmap(mFlattenedBackground, null, bounds, paint); 335 paint.setColorFilter(oldFilter); 336 } 337 } 338 339 private static class ClockIconDrawable extends FastBitmapDrawable implements Runnable { 340 341 private final Calendar mTime = Calendar.getInstance(); 342 343 private final ClockBitmapInfo mInfo; 344 345 private final AdaptiveIconDrawable mFullDrawable; 346 private final LayerDrawable mForeground; 347 ClockIconDrawable(ClockBitmapInfo clockInfo)348 ClockIconDrawable(ClockBitmapInfo clockInfo) { 349 super(clockInfo); 350 351 mInfo = clockInfo; 352 mFullDrawable = (AdaptiveIconDrawable) mInfo.animInfo.baseDrawableState 353 .newDrawable().mutate(); 354 mForeground = (LayerDrawable) mFullDrawable.getForeground(); 355 } 356 357 @Override onBoundsChange(Rect bounds)358 protected void onBoundsChange(Rect bounds) { 359 super.onBoundsChange(bounds); 360 mFullDrawable.setBounds(bounds); 361 } 362 363 @Override drawInternal(Canvas canvas, Rect bounds)364 public void drawInternal(Canvas canvas, Rect bounds) { 365 if (mInfo == null) { 366 super.drawInternal(canvas, bounds); 367 return; 368 } 369 mInfo.drawBackground(canvas, bounds, mPaint); 370 371 // prepare and draw the foreground 372 mInfo.animInfo.applyTime(mTime, mForeground); 373 374 int saveCount = canvas.save(); 375 canvas.scale(mInfo.scale, mInfo.scale, 376 bounds.exactCenterX() + mInfo.offset, bounds.exactCenterY() + mInfo.offset); 377 canvas.clipPath(mFullDrawable.getIconMask()); 378 mForeground.setBounds(bounds); 379 mForeground.draw(canvas); 380 canvas.restoreToCount(saveCount); 381 382 reschedule(); 383 } 384 385 @Override isThemed()386 public boolean isThemed() { 387 return mInfo.bgFilter != null; 388 } 389 390 @Override updateFilter()391 protected void updateFilter() { 392 super.updateFilter(); 393 mFullDrawable.setColorFilter(mPaint.getColorFilter()); 394 } 395 396 @Override run()397 public void run() { 398 if (mInfo.animInfo.applyTime(mTime, mForeground)) { 399 invalidateSelf(); 400 } else { 401 reschedule(); 402 } 403 } 404 405 @Override setVisible(boolean visible, boolean restart)406 public boolean setVisible(boolean visible, boolean restart) { 407 boolean result = super.setVisible(visible, restart); 408 if (visible) { 409 reschedule(); 410 } else { 411 unscheduleSelf(this); 412 } 413 return result; 414 } 415 reschedule()416 private void reschedule() { 417 if (!isVisible()) { 418 return; 419 } 420 421 unscheduleSelf(this); 422 final long upTime = SystemClock.uptimeMillis(); 423 final long step = TICK_MS; /* tick every 200 ms */ 424 scheduleSelf(this, upTime - ((upTime % step)) + step); 425 } 426 427 @Override getConstantState()428 public ConstantState getConstantState() { 429 return new ClockConstantState(mInfo, isDisabled()); 430 } 431 432 private static class ClockConstantState extends FastBitmapConstantState { 433 434 private final ClockBitmapInfo mInfo; 435 ClockConstantState(ClockBitmapInfo info, boolean isDisabled)436 ClockConstantState(ClockBitmapInfo info, boolean isDisabled) { 437 super(info.icon, info.color, isDisabled); 438 mInfo = info; 439 } 440 441 @Override newDrawable()442 public FastBitmapDrawable newDrawable() { 443 ClockIconDrawable drawable = new ClockIconDrawable(mInfo); 444 drawable.setIsDisabled(mIsDisabled); 445 return drawable; 446 } 447 } 448 } 449 } 450