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