diff options
37 files changed, 5118 insertions, 2 deletions
diff --git a/PermissionController/Android.bp b/PermissionController/Android.bp index afd4658ae..c973ee8e0 100644 --- a/PermissionController/Android.bp +++ b/PermissionController/Android.bp @@ -92,7 +92,7 @@ android_app { ], static_libs: [ - "iconloader", + "iconloader_sc_mainline_prod", "com.google.android.material_material", "androidx.transition_transition", "androidx-constraintlayout_constraintlayout", diff --git a/PermissionController/iconloaderlib/.gitignore b/PermissionController/iconloaderlib/.gitignore new file mode 100644 index 000000000..6213826ab --- /dev/null +++ b/PermissionController/iconloaderlib/.gitignore @@ -0,0 +1,13 @@ +*.iml +.project +.classpath +.project.properties +gen/ +bin/ +.idea/ +.gradle/ +local.properties +gradle/ +build/ +gradlew* +.DS_Store diff --git a/PermissionController/iconloaderlib/Android.bp b/PermissionController/iconloaderlib/Android.bp new file mode 100644 index 000000000..2bc3de435 --- /dev/null +++ b/PermissionController/iconloaderlib/Android.bp @@ -0,0 +1,52 @@ +// Copyright (C) 2018 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +android_library { + name: "iconloader_base_sc_mainline_prod", + sdk_version: "current", + min_sdk_version: "26", + static_libs: [ + "androidx.core_core", + ], + resource_dirs: [ + "res", + ], + srcs: [ + "src/**/*.java", + ], +} + +android_library { + name: "iconloader_sc_mainline_prod", + sdk_version: "system_current", + min_sdk_version: "26", + static_libs: [ + "androidx.core_core", + ], + resource_dirs: [ + "res", + ], + srcs: [ + "src/**/*.java", + "src_full_lib/**/*.java", + ], + apex_available: [ + "//apex_available:platform", + "com.android.permission", + ], +} diff --git a/PermissionController/iconloaderlib/AndroidManifest.xml b/PermissionController/iconloaderlib/AndroidManifest.xml new file mode 100644 index 000000000..b30258da2 --- /dev/null +++ b/PermissionController/iconloaderlib/AndroidManifest.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.launcher3.icons"> +</manifest> diff --git a/PermissionController/iconloaderlib/build.gradle b/PermissionController/iconloaderlib/build.gradle new file mode 100644 index 000000000..84102758e --- /dev/null +++ b/PermissionController/iconloaderlib/build.gradle @@ -0,0 +1,38 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion COMPILE_SDK + buildToolsVersion BUILD_TOOLS_VERSION + + defaultConfig { + minSdkVersion 26 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + } + + sourceSets { + main { + java.srcDirs = ['src', 'src_full_lib'] + manifest.srcFile 'AndroidManifest.xml' + res.srcDirs = ['res'] + } + } + + lintOptions { + abortOnError false + } + + tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation "androidx.core:core:${ANDROID_X_VERSION}" +} diff --git a/PermissionController/iconloaderlib/res/drawable-v26/adaptive_icon_drawable_wrapper.xml b/PermissionController/iconloaderlib/res/drawable-v26/adaptive_icon_drawable_wrapper.xml new file mode 100644 index 000000000..9f13cf571 --- /dev/null +++ b/PermissionController/iconloaderlib/res/drawable-v26/adaptive_icon_drawable_wrapper.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2017 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <background android:drawable="@color/legacy_icon_background"/> + <foreground> + <com.android.launcher3.icons.FixedScaleDrawable /> + </foreground> +</adaptive-icon> diff --git a/PermissionController/iconloaderlib/res/drawable/ic_instant_app_badge.xml b/PermissionController/iconloaderlib/res/drawable/ic_instant_app_badge.xml new file mode 100644 index 000000000..b74317e5f --- /dev/null +++ b/PermissionController/iconloaderlib/res/drawable/ic_instant_app_badge.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2017 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="@dimen/profile_badge_size" + android:height="@dimen/profile_badge_size" + android:viewportWidth="18" + android:viewportHeight="18"> + + <path + android:fillColor="@android:color/black" + android:strokeWidth="1" + android:pathData="M 9 0 C 13.9705627485 0 18 4.02943725152 18 9 C 18 13.9705627485 13.9705627485 18 9 18 C 4.02943725152 18 0 13.9705627485 0 9 C 0 4.02943725152 4.02943725152 0 9 0 Z" /> + <path + android:fillColor="@android:color/white" + android:strokeWidth="1" + android:pathData="M 9 0 C 13.9705627485 0 18 4.02943725152 18 9 C 18 13.9705627485 13.9705627485 18 9 18 C 4.02943725152 18 0 13.9705627485 0 9 C 0 4.02943725152 4.02943725152 0 9 0 Z" /> + <path + android:fillColor="@android:color/white" + android:strokeWidth="1" + android:pathData="M 9 0 C 13.9705627485 0 18 4.02943725152 18 9 C 18 13.9705627485 13.9705627485 18 9 18 C 4.02943725152 18 0 13.9705627485 0 9 C 0 4.02943725152 4.02943725152 0 9 0 Z" /> + <path + android:fillColor="@android:color/black" + android:fillAlpha="0.87" + android:strokeWidth="1" + android:pathData="M 6 10.4123279 L 8.63934949 10.4123279 L 8.63934949 15.6 L 12.5577168 7.84517705 L 9.94547194 7.84517705 L 9.94547194 2 Z" /> +</vector> diff --git a/PermissionController/iconloaderlib/res/values/attrs.xml b/PermissionController/iconloaderlib/res/values/attrs.xml new file mode 100644 index 000000000..8f0bd2c1a --- /dev/null +++ b/PermissionController/iconloaderlib/res/values/attrs.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +** +** Copyright 2021, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ +--> +<resources> + <attr name="disabledIconAlpha" format="float" /> + <attr name="loadingIconColor" format="color" /> + +</resources>
\ No newline at end of file diff --git a/PermissionController/iconloaderlib/res/values/colors.xml b/PermissionController/iconloaderlib/res/values/colors.xml new file mode 100644 index 000000000..70582c2e2 --- /dev/null +++ b/PermissionController/iconloaderlib/res/values/colors.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +** +** Copyright 2018, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ +--> +<resources> + <color name="legacy_icon_background">#FFFFFF</color> + + <!-- Yellow 600, used for highlighting "important" conversations in settings & notifications --> + <color name="important_conversation">#f9ab00</color> +</resources> diff --git a/PermissionController/iconloaderlib/res/values/config.xml b/PermissionController/iconloaderlib/res/values/config.xml new file mode 100644 index 000000000..893f955c2 --- /dev/null +++ b/PermissionController/iconloaderlib/res/values/config.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +** +** Copyright 2018, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ +--> +<resources> + + <!-- Various configurations to control the simple cache implementation --> + + <dimen name="default_icon_bitmap_size">56dp</dimen> + <bool name="simple_cache_enable_im_memory">false</bool> + <string name="cache_db_name" translatable="false">app_icons.db</string> + + <string name="calendar_component_name" translatable="false"></string> + <string name="clock_component_name" translatable="false"></string> + +</resources>
\ No newline at end of file diff --git a/PermissionController/iconloaderlib/res/values/dimens.xml b/PermissionController/iconloaderlib/res/values/dimens.xml new file mode 100644 index 000000000..e8c0c44f7 --- /dev/null +++ b/PermissionController/iconloaderlib/res/values/dimens.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources> + <dimen name="profile_badge_size">24dp</dimen> +</resources> diff --git a/PermissionController/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java new file mode 100644 index 000000000..9ce997587 --- /dev/null +++ b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java @@ -0,0 +1,466 @@ +package com.android.launcher3.icons; + +import static android.graphics.Paint.DITHER_FLAG; +import static android.graphics.Paint.FILTER_BITMAP_FLAG; + +import static com.android.launcher3.icons.ShadowGenerator.BLUR_FACTOR; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PaintFlagsDrawFilter; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.AdaptiveIconDrawable; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; +import android.os.Build; +import android.os.Process; +import android.os.UserHandle; + +import androidx.annotation.NonNull; + +import com.android.launcher3.icons.BitmapInfo.Extender; + +/** + * This class will be moved to androidx library. There shouldn't be any dependency outside + * this package. + */ +public class BaseIconFactory implements AutoCloseable { + + private static final String TAG = "BaseIconFactory"; + private static final int DEFAULT_WRAPPER_BACKGROUND = Color.WHITE; + static final boolean ATLEAST_OREO = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; + static final boolean ATLEAST_P = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P; + + private static final float ICON_BADGE_SCALE = 0.444f; + + private final Rect mOldBounds = new Rect(); + protected final Context mContext; + private final Canvas mCanvas; + private final PackageManager mPm; + private final ColorExtractor mColorExtractor; + private boolean mDisableColorExtractor; + private boolean mBadgeOnLeft = false; + + protected final int mFillResIconDpi; + protected final int mIconBitmapSize; + + private IconNormalizer mNormalizer; + private ShadowGenerator mShadowGenerator; + private final boolean mShapeDetection; + + private Drawable mWrapperIcon; + private int mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND; + private Bitmap mUserBadgeBitmap; + + private final Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); + private static final float PLACEHOLDER_TEXT_SIZE = 20f; + private static int PLACEHOLDER_BACKGROUND_COLOR = Color.rgb(240, 240, 240); + + protected BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize, + boolean shapeDetection) { + mContext = context.getApplicationContext(); + mShapeDetection = shapeDetection; + mFillResIconDpi = fillResIconDpi; + mIconBitmapSize = iconBitmapSize; + + mPm = mContext.getPackageManager(); + mColorExtractor = new ColorExtractor(); + + mCanvas = new Canvas(); + mCanvas.setDrawFilter(new PaintFlagsDrawFilter(DITHER_FLAG, FILTER_BITMAP_FLAG)); + mTextPaint.setTextAlign(Paint.Align.CENTER); + mTextPaint.setColor(PLACEHOLDER_BACKGROUND_COLOR); + mTextPaint.setTextSize(context.getResources().getDisplayMetrics().density * + PLACEHOLDER_TEXT_SIZE); + clear(); + } + + public BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize) { + this(context, fillResIconDpi, iconBitmapSize, false); + } + + protected void clear() { + mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND; + mDisableColorExtractor = false; + mBadgeOnLeft = false; + } + + public ShadowGenerator getShadowGenerator() { + if (mShadowGenerator == null) { + mShadowGenerator = new ShadowGenerator(mIconBitmapSize); + } + return mShadowGenerator; + } + + public IconNormalizer getNormalizer() { + if (mNormalizer == null) { + mNormalizer = new IconNormalizer(mContext, mIconBitmapSize, mShapeDetection); + } + return mNormalizer; + } + + @SuppressWarnings("deprecation") + public BitmapInfo createIconBitmap(Intent.ShortcutIconResource iconRes) { + try { + Resources resources = mPm.getResourcesForApplication(iconRes.packageName); + if (resources != null) { + final int id = resources.getIdentifier(iconRes.resourceName, null, null); + // do not stamp old legacy shortcuts as the app may have already forgotten about it + return createBadgedIconBitmap( + resources.getDrawableForDensity(id, mFillResIconDpi), + Process.myUserHandle() /* only available on primary user */, + false /* do not apply legacy treatment */); + } + } catch (Exception e) { + // Icon not found. + } + return null; + } + + /** + * Create a placeholder icon using the passed in text. + * + * @param placeholder used for foreground element in the icon bitmap + * @param color used for the foreground text color + * @return + */ + public BitmapInfo createIconBitmap(String placeholder, int color) { + if (!ATLEAST_OREO) return null; + + Bitmap placeholderBitmap = Bitmap.createBitmap(mIconBitmapSize, mIconBitmapSize, + Bitmap.Config.ARGB_8888); + mTextPaint.setColor(color); + Canvas canvas = new Canvas(placeholderBitmap); + canvas.drawText(placeholder, mIconBitmapSize / 2, mIconBitmapSize * 5 / 8, mTextPaint); + AdaptiveIconDrawable drawable = new AdaptiveIconDrawable( + new ColorDrawable(PLACEHOLDER_BACKGROUND_COLOR), + new BitmapDrawable(mContext.getResources(), placeholderBitmap)); + Bitmap icon = createIconBitmap(drawable, 1f); + return BitmapInfo.of(icon, extractColor(icon)); + } + + public BitmapInfo createIconBitmap(Bitmap icon) { + if (mIconBitmapSize != icon.getWidth() || mIconBitmapSize != icon.getHeight()) { + icon = createIconBitmap(new BitmapDrawable(mContext.getResources(), icon), 1f); + } + + return BitmapInfo.of(icon, extractColor(icon)); + } + + /** + * Creates an icon from the bitmap cropped to the current device icon shape + */ + public BitmapInfo createShapedIconBitmap(Bitmap icon, UserHandle user) { + Drawable d = new FixedSizeBitmapDrawable(icon); + if (ATLEAST_OREO) { + float inset = AdaptiveIconDrawable.getExtraInsetFraction(); + inset = inset / (1 + 2 * inset); + d = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), + new InsetDrawable(d, inset, inset, inset, inset)); + } + return createBadgedIconBitmap(d, user, true); + } + + public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user, + boolean shrinkNonAdaptiveIcons) { + return createBadgedIconBitmap(icon, user, shrinkNonAdaptiveIcons, false, null); + } + + public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user, + int iconAppTargetSdk) { + return createBadgedIconBitmap(icon, user, iconAppTargetSdk, false); + } + + public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user, + int iconAppTargetSdk, boolean isInstantApp) { + return createBadgedIconBitmap(icon, user, iconAppTargetSdk, isInstantApp, null); + } + + public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user, + int iconAppTargetSdk, boolean isInstantApp, float[] scale) { + boolean shrinkNonAdaptiveIcons = ATLEAST_P || + (ATLEAST_OREO && iconAppTargetSdk >= Build.VERSION_CODES.O); + return createBadgedIconBitmap(icon, user, shrinkNonAdaptiveIcons, isInstantApp, scale); + } + + public Bitmap createScaledBitmapWithoutShadow(Drawable icon, int iconAppTargetSdk) { + boolean shrinkNonAdaptiveIcons = ATLEAST_P || + (ATLEAST_OREO && iconAppTargetSdk >= Build.VERSION_CODES.O); + return createScaledBitmapWithoutShadow(icon, shrinkNonAdaptiveIcons); + } + + /** + * Creates bitmap using the source drawable and various parameters. + * The bitmap is visually normalized with other icons and has enough spacing to add shadow. + * + * @param icon source of the icon + * @param user info can be used for a badge + * @param shrinkNonAdaptiveIcons {@code true} if non adaptive icons should be treated + * @param isInstantApp info can be used for a badge + * @param scale returns the scale result from normalization + * @return a bitmap suitable for disaplaying as an icon at various system UIs. + */ + public BitmapInfo createBadgedIconBitmap(@NonNull Drawable icon, UserHandle user, + boolean shrinkNonAdaptiveIcons, boolean isInstantApp, float[] scale) { + if (scale == null) { + scale = new float[1]; + } + icon = normalizeAndWrapToAdaptiveIcon(icon, shrinkNonAdaptiveIcons, null, scale); + Bitmap bitmap = createIconBitmap(icon, scale[0]); + if (ATLEAST_OREO && icon instanceof AdaptiveIconDrawable) { + mCanvas.setBitmap(bitmap); + getShadowGenerator().recreateIcon(Bitmap.createBitmap(bitmap), mCanvas); + mCanvas.setBitmap(null); + } + + if (isInstantApp) { + badgeWithDrawable(bitmap, mContext.getDrawable(R.drawable.ic_instant_app_badge)); + } + if (user != null) { + BitmapDrawable drawable = new FixedSizeBitmapDrawable(bitmap); + Drawable badged = mPm.getUserBadgedIcon(drawable, user); + if (badged instanceof BitmapDrawable) { + bitmap = ((BitmapDrawable) badged).getBitmap(); + } else { + bitmap = createIconBitmap(badged, 1f); + } + } + int color = extractColor(bitmap); + return icon instanceof BitmapInfo.Extender + ? ((BitmapInfo.Extender) icon).getExtendedInfo(bitmap, color, this, scale[0], user) + : BitmapInfo.of(bitmap, color); + } + + public Bitmap getUserBadgeBitmap(UserHandle user) { + if (mUserBadgeBitmap == null) { + Bitmap bitmap = Bitmap.createBitmap( + mIconBitmapSize, mIconBitmapSize, Bitmap.Config.ARGB_8888); + Drawable badgedDrawable = mPm.getUserBadgedIcon( + new FixedSizeBitmapDrawable(bitmap), user); + if (badgedDrawable instanceof BitmapDrawable) { + mUserBadgeBitmap = ((BitmapDrawable) badgedDrawable).getBitmap(); + } else { + badgedDrawable.setBounds(0, 0, mIconBitmapSize, mIconBitmapSize); + mUserBadgeBitmap = BitmapRenderer.createSoftwareBitmap( + mIconBitmapSize, mIconBitmapSize, badgedDrawable::draw); + } + } + return mUserBadgeBitmap; + } + + public Bitmap createScaledBitmapWithoutShadow(Drawable icon, boolean shrinkNonAdaptiveIcons) { + RectF iconBounds = new RectF(); + float[] scale = new float[1]; + icon = normalizeAndWrapToAdaptiveIcon(icon, shrinkNonAdaptiveIcons, iconBounds, scale); + return createIconBitmap(icon, + Math.min(scale[0], ShadowGenerator.getScaleForBounds(iconBounds))); + } + + /** + * Switches badging to left/right + */ + public void setBadgeOnLeft(boolean badgeOnLeft) { + mBadgeOnLeft = badgeOnLeft; + } + + /** + * Sets the background color used for wrapped adaptive icon + */ + public void setWrapperBackgroundColor(int color) { + mWrapperBackgroundColor = (Color.alpha(color) < 255) ? DEFAULT_WRAPPER_BACKGROUND : color; + } + + /** + * Disables the dominant color extraction for all icons loaded. + */ + public void disableColorExtraction() { + mDisableColorExtractor = true; + } + + private Drawable normalizeAndWrapToAdaptiveIcon(@NonNull Drawable icon, + boolean shrinkNonAdaptiveIcons, RectF outIconBounds, float[] outScale) { + if (icon == null) { + return null; + } + float scale = 1f; + + if (shrinkNonAdaptiveIcons && ATLEAST_OREO) { + if (mWrapperIcon == null) { + mWrapperIcon = mContext.getDrawable(R.drawable.adaptive_icon_drawable_wrapper) + .mutate(); + } + AdaptiveIconDrawable dr = (AdaptiveIconDrawable) mWrapperIcon; + dr.setBounds(0, 0, 1, 1); + boolean[] outShape = new boolean[1]; + scale = getNormalizer().getScale(icon, outIconBounds, dr.getIconMask(), outShape); + if (!(icon instanceof AdaptiveIconDrawable) && !outShape[0]) { + FixedScaleDrawable fsd = ((FixedScaleDrawable) dr.getForeground()); + fsd.setDrawable(icon); + fsd.setScale(scale); + icon = dr; + scale = getNormalizer().getScale(icon, outIconBounds, null, null); + + ((ColorDrawable) dr.getBackground()).setColor(mWrapperBackgroundColor); + } + } else { + scale = getNormalizer().getScale(icon, outIconBounds, null, null); + } + + outScale[0] = scale; + return icon; + } + + /** + * Adds the {@param badge} on top of {@param target} using the badge dimensions. + */ + public void badgeWithDrawable(Bitmap target, Drawable badge) { + mCanvas.setBitmap(target); + badgeWithDrawable(mCanvas, badge); + mCanvas.setBitmap(null); + } + + /** + * Adds the {@param badge} on top of {@param target} using the badge dimensions. + */ + public void badgeWithDrawable(Canvas target, Drawable badge) { + int badgeSize = getBadgeSizeForIconSize(mIconBitmapSize); + if (mBadgeOnLeft) { + badge.setBounds(0, mIconBitmapSize - badgeSize, badgeSize, mIconBitmapSize); + } else { + badge.setBounds(mIconBitmapSize - badgeSize, mIconBitmapSize - badgeSize, + mIconBitmapSize, mIconBitmapSize); + } + badge.draw(target); + } + + private Bitmap createIconBitmap(Drawable icon, float scale) { + return createIconBitmap(icon, scale, mIconBitmapSize); + } + + /** + * @param icon drawable that should be flattened to a bitmap + * @param scale the scale to apply before drawing {@param icon} on the canvas + */ + public Bitmap createIconBitmap(@NonNull Drawable icon, float scale, int size) { + Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + if (icon == null) { + return bitmap; + } + mCanvas.setBitmap(bitmap); + mOldBounds.set(icon.getBounds()); + + if (ATLEAST_OREO && icon instanceof AdaptiveIconDrawable) { + int offset = Math.max((int) Math.ceil(BLUR_FACTOR * size), + Math.round(size * (1 - scale) / 2 )); + icon.setBounds(offset, offset, size - offset, size - offset); + if (icon instanceof BitmapInfo.Extender) { + ((Extender) icon).drawForPersistence(mCanvas); + } else { + icon.draw(mCanvas); + } + } else { + if (icon instanceof BitmapDrawable) { + BitmapDrawable bitmapDrawable = (BitmapDrawable) icon; + Bitmap b = bitmapDrawable.getBitmap(); + if (bitmap != null && b.getDensity() == Bitmap.DENSITY_NONE) { + bitmapDrawable.setTargetDensity(mContext.getResources().getDisplayMetrics()); + } + } + int width = size; + int height = size; + + int intrinsicWidth = icon.getIntrinsicWidth(); + int intrinsicHeight = icon.getIntrinsicHeight(); + if (intrinsicWidth > 0 && intrinsicHeight > 0) { + // Scale the icon proportionally to the icon dimensions + final float ratio = (float) intrinsicWidth / intrinsicHeight; + if (intrinsicWidth > intrinsicHeight) { + height = (int) (width / ratio); + } else if (intrinsicHeight > intrinsicWidth) { + width = (int) (height * ratio); + } + } + final int left = (size - width) / 2; + final int top = (size - height) / 2; + icon.setBounds(left, top, left + width, top + height); + mCanvas.save(); + mCanvas.scale(scale, scale, size / 2, size / 2); + icon.draw(mCanvas); + mCanvas.restore(); + + } + icon.setBounds(mOldBounds); + mCanvas.setBitmap(null); + return bitmap; + } + + @Override + public void close() { + clear(); + } + + public BitmapInfo makeDefaultIcon(UserHandle user) { + return createBadgedIconBitmap(getFullResDefaultActivityIcon(mFillResIconDpi), + user, Build.VERSION.SDK_INT); + } + + public static Drawable getFullResDefaultActivityIcon(int iconDpi) { + return Resources.getSystem().getDrawableForDensity( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + ? android.R.drawable.sym_def_app_icon : android.R.mipmap.sym_def_app_icon, + iconDpi); + } + + /** + * Badges the provided source with the badge info + */ + public BitmapInfo badgeBitmap(Bitmap source, BitmapInfo badgeInfo) { + Bitmap icon = BitmapRenderer.createHardwareBitmap(mIconBitmapSize, mIconBitmapSize, (c) -> { + getShadowGenerator().recreateIcon(source, c); + badgeWithDrawable(c, new FixedSizeBitmapDrawable(badgeInfo.icon)); + }); + return BitmapInfo.of(icon, badgeInfo.color); + } + + private int extractColor(Bitmap bitmap) { + return mDisableColorExtractor ? 0 : mColorExtractor.findDominantColorByHue(bitmap); + } + + /** + * Returns the correct badge size given an icon size + */ + public static int getBadgeSizeForIconSize(int iconSize) { + return (int) (ICON_BADGE_SCALE * iconSize); + } + + /** + * An extension of {@link BitmapDrawable} which returns the bitmap pixel size as intrinsic size. + * This allows the badging to be done based on the action bitmap size rather than + * the scaled bitmap size. + */ + private static class FixedSizeBitmapDrawable extends BitmapDrawable { + + public FixedSizeBitmapDrawable(Bitmap bitmap) { + super(null, bitmap); + } + + @Override + public int getIntrinsicHeight() { + return getBitmap().getWidth(); + } + + @Override + public int getIntrinsicWidth() { + return getBitmap().getWidth(); + } + } +} diff --git a/PermissionController/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java new file mode 100644 index 000000000..06b39b8f1 --- /dev/null +++ b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.icons; + +import static com.android.launcher3.icons.GraphicsUtils.getExpectedBitmapSize; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.UserHandle; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.launcher3.icons.ThemedIconDrawable.ThemedBitmapInfo; +import com.android.launcher3.icons.cache.BaseIconCache; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class BitmapInfo { + + public static final Bitmap LOW_RES_ICON = Bitmap.createBitmap(1, 1, Config.ALPHA_8); + public static final BitmapInfo LOW_RES_INFO = fromBitmap(LOW_RES_ICON); + + public static final String TAG = "BitmapInfo"; + + protected static final byte TYPE_DEFAULT = 1; + protected static final byte TYPE_THEMED = 2; + + public final Bitmap icon; + public final int color; + + public BitmapInfo(Bitmap icon, int color) { + this.icon = icon; + this.color = color; + } + + /** + * Ideally icon should not be null, except in cases when generating hardware bitmap failed + */ + public final boolean isNullOrLowRes() { + return icon == null || icon == LOW_RES_ICON; + } + + public final boolean isLowRes() { + return LOW_RES_ICON == icon; + } + + /** + * Returns a serialized version of BitmapInfo + */ + @Nullable + public byte[] toByteArray() { + if (isNullOrLowRes()) { + return null; + } + ByteArrayOutputStream out = new ByteArrayOutputStream(getExpectedBitmapSize(icon) + 1); + try { + out.write(TYPE_DEFAULT); + icon.compress(Bitmap.CompressFormat.PNG, 100, out); + out.flush(); + out.close(); + return out.toByteArray(); + } catch (IOException e) { + Log.w(TAG, "Could not write bitmap"); + return null; + } + } + + /** + * Returns a new icon based on the theme of the context + */ + public FastBitmapDrawable newThemedIcon(Context context) { + return newIcon(context); + } + + /** + * Creates a drawable for the provided BitmapInfo + */ + public FastBitmapDrawable newIcon(Context context) { + FastBitmapDrawable drawable = isLowRes() + ? new PlaceHolderIconDrawable(this, context) + : new FastBitmapDrawable(this); + drawable.mDisabledAlpha = GraphicsUtils.getFloat(context, R.attr.disabledIconAlpha, 1f); + return drawable; + } + + /** + * Returns a BitmapInfo previously serialized using {@link #toByteArray()}; + */ + @NonNull + public static BitmapInfo fromByteArray(byte[] data, int color, UserHandle user, + BaseIconCache iconCache, Context context) { + if (data == null) { + return null; + } + BitmapFactory.Options decodeOptions; + if (BitmapRenderer.USE_HARDWARE_BITMAP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + decodeOptions = new BitmapFactory.Options(); + decodeOptions.inPreferredConfig = Bitmap.Config.HARDWARE; + } else { + decodeOptions = null; + } + if (data[0] == TYPE_DEFAULT) { + return BitmapInfo.of( + BitmapFactory.decodeByteArray(data, 1, data.length - 1, decodeOptions), + color); + } else if (data[0] == TYPE_THEMED) { + return ThemedBitmapInfo.decode(data, color, decodeOptions, user, iconCache, context); + } else { + return null; + } + } + + public static BitmapInfo fromBitmap(@NonNull Bitmap bitmap) { + return of(bitmap, 0); + } + + public static BitmapInfo of(@NonNull Bitmap bitmap, int color) { + return new BitmapInfo(bitmap, color); + } + + /** + * Interface to be implemented by drawables to provide a custom BitmapInfo + */ + public interface Extender { + + /** + * Called for creating a custom BitmapInfo + */ + BitmapInfo getExtendedInfo(Bitmap bitmap, int color, + BaseIconFactory iconFactory, float normalizationScale, UserHandle user); + + /** + * Called to draw the UI independent of any runtime configurations like time or theme + */ + void drawForPersistence(Canvas canvas); + + /** + * Returns a new icon with theme applied + */ + Drawable getThemedDrawable(Context context); + } +} diff --git a/PermissionController/iconloaderlib/src/com/android/launcher3/icons/BitmapRenderer.java b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/BitmapRenderer.java new file mode 100644 index 000000000..5751ed95c --- /dev/null +++ b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/BitmapRenderer.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.icons; + +import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; +import android.graphics.Picture; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Build; +import android.os.Build.VERSION_CODES; + +/** + * Interface representing a bitmap draw operation. + */ +public interface BitmapRenderer { + + boolean USE_HARDWARE_BITMAP = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P; + + static Bitmap createSoftwareBitmap(int width, int height, BitmapRenderer renderer) { + GraphicsUtils.noteNewBitmapCreated(); + Bitmap result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + renderer.draw(new Canvas(result)); + return result; + } + + @TargetApi(Build.VERSION_CODES.P) + static Bitmap createHardwareBitmap(int width, int height, BitmapRenderer renderer) { + if (!USE_HARDWARE_BITMAP) { + return createSoftwareBitmap(width, height, renderer); + } + + GraphicsUtils.noteNewBitmapCreated(); + Picture picture = new Picture(); + renderer.draw(picture.beginRecording(width, height)); + picture.endRecording(); + return Bitmap.createBitmap(picture); + } + + /** + * Returns a bitmap from subset of the source bitmap. The new bitmap may be the + * same object as source, or a copy may have been made. + */ + static Bitmap createBitmap(Bitmap source, int x, int y, int width, int height) { + if (Build.VERSION.SDK_INT >= VERSION_CODES.O && source.getConfig() == Config.HARDWARE) { + return createHardwareBitmap(width, height, c -> c.drawBitmap(source, + new Rect(x, y, x + width, y + height), new RectF(0, 0, width, height), null)); + } else { + GraphicsUtils.noteNewBitmapCreated(); + return Bitmap.createBitmap(source, x, y, width, height); + } + } + + void draw(Canvas out); +} diff --git a/PermissionController/iconloaderlib/src/com/android/launcher3/icons/ClockDrawableWrapper.java b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/ClockDrawableWrapper.java new file mode 100644 index 000000000..a7894c991 --- /dev/null +++ b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/ClockDrawableWrapper.java @@ -0,0 +1,448 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.icons; + +import static com.android.launcher3.icons.ThemedIconDrawable.getColors; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuff.Mode; +import android.graphics.PorterDuffColorFilter; +import android.graphics.Rect; +import android.graphics.drawable.AdaptiveIconDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.os.Build; +import android.os.Bundle; +import android.os.Process; +import android.os.SystemClock; +import android.os.UserHandle; +import android.util.Log; +import android.util.TypedValue; + +import androidx.annotation.Nullable; + +import com.android.launcher3.icons.ThemedIconDrawable.ThemeData; + +import java.util.Calendar; +import java.util.concurrent.TimeUnit; +import java.util.function.IntFunction; + +/** + * Wrapper over {@link AdaptiveIconDrawable} to intercept icon flattening logic for dynamic + * clock icons + */ +@TargetApi(Build.VERSION_CODES.O) +public class ClockDrawableWrapper extends AdaptiveIconDrawable implements BitmapInfo.Extender { + + private static final String TAG = "ClockDrawableWrapper"; + + private static final boolean DISABLE_SECONDS = true; + + // Time after which the clock icon should check for an update. The actual invalidate + // will only happen in case of any change. + public static final long TICK_MS = DISABLE_SECONDS ? TimeUnit.MINUTES.toMillis(1) : 200L; + + private static final String LAUNCHER_PACKAGE = "com.android.launcher3"; + private static final String ROUND_ICON_METADATA_KEY = LAUNCHER_PACKAGE + + ".LEVEL_PER_TICK_ICON_ROUND"; + private static final String HOUR_INDEX_METADATA_KEY = LAUNCHER_PACKAGE + ".HOUR_LAYER_INDEX"; + private static final String MINUTE_INDEX_METADATA_KEY = LAUNCHER_PACKAGE + + ".MINUTE_LAYER_INDEX"; + private static final String SECOND_INDEX_METADATA_KEY = LAUNCHER_PACKAGE + + ".SECOND_LAYER_INDEX"; + private static final String DEFAULT_HOUR_METADATA_KEY = LAUNCHER_PACKAGE + + ".DEFAULT_HOUR"; + private static final String DEFAULT_MINUTE_METADATA_KEY = LAUNCHER_PACKAGE + + ".DEFAULT_MINUTE"; + private static final String DEFAULT_SECOND_METADATA_KEY = LAUNCHER_PACKAGE + + ".DEFAULT_SECOND"; + + /* Number of levels to jump per second for the second hand */ + private static final int LEVELS_PER_SECOND = 10; + + public static final int INVALID_VALUE = -1; + + private final AnimationInfo mAnimationInfo = new AnimationInfo(); + private int mTargetSdkVersion; + protected ThemeData mThemeData; + + public ClockDrawableWrapper(AdaptiveIconDrawable base) { + super(base.getBackground(), base.getForeground()); + } + + /** + * Loads and returns the wrapper from the provided package, or returns null + * if it is unable to load. + */ + public static ClockDrawableWrapper forPackage(Context context, String pkg, int iconDpi) { + try { + PackageManager pm = context.getPackageManager(); + ApplicationInfo appInfo = pm.getApplicationInfo(pkg, + PackageManager.MATCH_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA); + Resources res = pm.getResourcesForApplication(appInfo); + return forExtras(appInfo, appInfo.metaData, + resId -> res.getDrawableForDensity(resId, iconDpi)); + } catch (Exception e) { + Log.d(TAG, "Unable to load clock drawable info", e); + } + return null; + } + + private static ClockDrawableWrapper fromThemeData(Context context, ThemeData themeData) { + try { + TypedArray ta = themeData.mResources.obtainTypedArray(themeData.mResID); + int count = ta.length(); + Bundle extras = new Bundle(); + for (int i = 0; i < count; i += 2) { + TypedValue v = ta.peekValue(i + 1); + extras.putInt(ta.getString(i), v.type >= TypedValue.TYPE_FIRST_INT + && v.type <= TypedValue.TYPE_LAST_INT + ? v.data : v.resourceId); + } + ta.recycle(); + ClockDrawableWrapper drawable = ClockDrawableWrapper.forExtras( + context.getApplicationInfo(), extras, resId -> { + int[] colors = getColors(context); + Drawable bg = new ColorDrawable(colors[0]); + Drawable fg = themeData.mResources.getDrawable(resId).mutate(); + fg.setTint(colors[1]); + return new AdaptiveIconDrawable(bg, fg); + }); + if (drawable != null) { + return drawable; + } + } catch (Exception e) { + Log.e(TAG, "Error loading themed clock", e); + } + return null; + } + + private static ClockDrawableWrapper forExtras(ApplicationInfo appInfo, Bundle metadata, + IntFunction<Drawable> drawableProvider) { + if (metadata == null) { + return null; + } + int drawableId = metadata.getInt(ROUND_ICON_METADATA_KEY, 0); + if (drawableId == 0) { + return null; + } + + Drawable drawable = drawableProvider.apply(drawableId).mutate(); + if (!(drawable instanceof AdaptiveIconDrawable)) { + return null; + } + + ClockDrawableWrapper wrapper = + new ClockDrawableWrapper((AdaptiveIconDrawable) drawable); + wrapper.mTargetSdkVersion = appInfo.targetSdkVersion; + AnimationInfo info = wrapper.mAnimationInfo; + + info.baseDrawableState = drawable.getConstantState(); + + info.hourLayerIndex = metadata.getInt(HOUR_INDEX_METADATA_KEY, INVALID_VALUE); + info.minuteLayerIndex = metadata.getInt(MINUTE_INDEX_METADATA_KEY, INVALID_VALUE); + info.secondLayerIndex = metadata.getInt(SECOND_INDEX_METADATA_KEY, INVALID_VALUE); + + info.defaultHour = metadata.getInt(DEFAULT_HOUR_METADATA_KEY, 0); + info.defaultMinute = metadata.getInt(DEFAULT_MINUTE_METADATA_KEY, 0); + info.defaultSecond = metadata.getInt(DEFAULT_SECOND_METADATA_KEY, 0); + + LayerDrawable foreground = (LayerDrawable) wrapper.getForeground(); + int layerCount = foreground.getNumberOfLayers(); + if (info.hourLayerIndex < 0 || info.hourLayerIndex >= layerCount) { + info.hourLayerIndex = INVALID_VALUE; + } + if (info.minuteLayerIndex < 0 || info.minuteLayerIndex >= layerCount) { + info.minuteLayerIndex = INVALID_VALUE; + } + if (info.secondLayerIndex < 0 || info.secondLayerIndex >= layerCount) { + info.secondLayerIndex = INVALID_VALUE; + } else if (DISABLE_SECONDS) { + foreground.setDrawable(info.secondLayerIndex, null); + info.secondLayerIndex = INVALID_VALUE; + } + info.applyTime(Calendar.getInstance(), foreground); + return wrapper; + } + + @Override + public ClockBitmapInfo getExtendedInfo(Bitmap bitmap, int color, + BaseIconFactory iconFactory, float normalizationScale, UserHandle user) { + iconFactory.disableColorExtraction(); + AdaptiveIconDrawable background = new AdaptiveIconDrawable( + getBackground().getConstantState().newDrawable(), null); + BitmapInfo bitmapInfo = iconFactory.createBadgedIconBitmap(background, + Process.myUserHandle(), mTargetSdkVersion, false); + + return new ClockBitmapInfo(bitmap, color, normalizationScale, + mAnimationInfo, bitmapInfo.icon, mThemeData); + } + + @Override + public void drawForPersistence(Canvas canvas) { + LayerDrawable foreground = (LayerDrawable) getForeground(); + resetLevel(foreground, mAnimationInfo.hourLayerIndex); + resetLevel(foreground, mAnimationInfo.minuteLayerIndex); + resetLevel(foreground, mAnimationInfo.secondLayerIndex); + draw(canvas); + mAnimationInfo.applyTime(Calendar.getInstance(), (LayerDrawable) getForeground()); + } + + @Override + public Drawable getThemedDrawable(Context context) { + if (mThemeData != null) { + ClockDrawableWrapper drawable = fromThemeData(context, mThemeData); + return drawable == null ? this : drawable; + } + return this; + } + + private void resetLevel(LayerDrawable drawable, int index) { + if (index != INVALID_VALUE) { + drawable.getDrawable(index).setLevel(0); + } + } + + private static class AnimationInfo { + + public ConstantState baseDrawableState; + + public int hourLayerIndex; + public int minuteLayerIndex; + public int secondLayerIndex; + public int defaultHour; + public int defaultMinute; + public int defaultSecond; + + boolean applyTime(Calendar time, LayerDrawable foregroundDrawable) { + time.setTimeInMillis(System.currentTimeMillis()); + + // We need to rotate by the difference from the default time if one is specified. + int convertedHour = (time.get(Calendar.HOUR) + (12 - defaultHour)) % 12; + int convertedMinute = (time.get(Calendar.MINUTE) + (60 - defaultMinute)) % 60; + int convertedSecond = (time.get(Calendar.SECOND) + (60 - defaultSecond)) % 60; + + boolean invalidate = false; + if (hourLayerIndex != INVALID_VALUE) { + final Drawable hour = foregroundDrawable.getDrawable(hourLayerIndex); + if (hour.setLevel(convertedHour * 60 + time.get(Calendar.MINUTE))) { + invalidate = true; + } + } + + if (minuteLayerIndex != INVALID_VALUE) { + final Drawable minute = foregroundDrawable.getDrawable(minuteLayerIndex); + if (minute.setLevel(time.get(Calendar.HOUR) * 60 + convertedMinute)) { + invalidate = true; + } + } + + if (secondLayerIndex != INVALID_VALUE) { + final Drawable second = foregroundDrawable.getDrawable(secondLayerIndex); + if (second.setLevel(convertedSecond * LEVELS_PER_SECOND)) { + invalidate = true; + } + } + + return invalidate; + } + } + + static class ClockBitmapInfo extends BitmapInfo { + + public final float scale; + public final int offset; + public final AnimationInfo animInfo; + public final Bitmap mFlattenedBackground; + + public final ThemeData themeData; + public final ColorFilter bgFilter; + + ClockBitmapInfo(Bitmap icon, int color, float scale, AnimationInfo animInfo, + Bitmap background, ThemeData themeData) { + this(icon, color, scale, animInfo, background, themeData, null); + } + + ClockBitmapInfo(Bitmap icon, int color, float scale, AnimationInfo animInfo, + Bitmap background, ThemeData themeData, ColorFilter bgFilter) { + super(icon, color); + this.scale = scale; + this.animInfo = animInfo; + this.offset = (int) Math.ceil(ShadowGenerator.BLUR_FACTOR * icon.getWidth()); + this.mFlattenedBackground = background; + this.themeData = themeData; + this.bgFilter = bgFilter; + } + + @Override + public FastBitmapDrawable newThemedIcon(Context context) { + if (themeData != null) { + ClockDrawableWrapper wrapper = fromThemeData(context, themeData); + if (wrapper != null) { + int[] colors = getColors(context); + ColorFilter bgFilter = new PorterDuffColorFilter(colors[0], Mode.SRC_ATOP); + return new ClockBitmapInfo(icon, colors[1], scale, + wrapper.mAnimationInfo, mFlattenedBackground, themeData, bgFilter) + .newIcon(context); + } + } + return super.newThemedIcon(context); + } + + @Override + public FastBitmapDrawable newIcon(Context context) { + ClockIconDrawable d = new ClockIconDrawable(this); + d.mDisabledAlpha = GraphicsUtils.getFloat(context, R.attr.disabledIconAlpha, 1f); + return d; + } + + @Nullable + @Override + public byte[] toByteArray() { + return null; + } + + void drawBackground(Canvas canvas, Rect bounds, Paint paint) { + // draw the background that is already flattened to a bitmap + ColorFilter oldFilter = paint.getColorFilter(); + if (bgFilter != null) { + paint.setColorFilter(bgFilter); + } + canvas.drawBitmap(mFlattenedBackground, null, bounds, paint); + paint.setColorFilter(oldFilter); + } + } + + private static class ClockIconDrawable extends FastBitmapDrawable implements Runnable { + + private final Calendar mTime = Calendar.getInstance(); + + private final ClockBitmapInfo mInfo; + + private final AdaptiveIconDrawable mFullDrawable; + private final LayerDrawable mForeground; + + ClockIconDrawable(ClockBitmapInfo clockInfo) { + super(clockInfo); + + mInfo = clockInfo; + mFullDrawable = (AdaptiveIconDrawable) mInfo.animInfo.baseDrawableState + .newDrawable().mutate(); + mForeground = (LayerDrawable) mFullDrawable.getForeground(); + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + mFullDrawable.setBounds(bounds); + } + + @Override + public void drawInternal(Canvas canvas, Rect bounds) { + if (mInfo == null) { + super.drawInternal(canvas, bounds); + return; + } + mInfo.drawBackground(canvas, bounds, mPaint); + + // prepare and draw the foreground + mInfo.animInfo.applyTime(mTime, mForeground); + + canvas.scale(mInfo.scale, mInfo.scale, + bounds.exactCenterX() + mInfo.offset, bounds.exactCenterY() + mInfo.offset); + canvas.clipPath(mFullDrawable.getIconMask()); + mForeground.draw(canvas); + + reschedule(); + } + + @Override + public boolean isThemed() { + return mInfo.bgFilter != null; + } + + @Override + protected void updateFilter() { + super.updateFilter(); + mFullDrawable.setColorFilter(mPaint.getColorFilter()); + } + + @Override + public void run() { + if (mInfo.animInfo.applyTime(mTime, mForeground)) { + invalidateSelf(); + } else { + reschedule(); + } + } + + @Override + public boolean setVisible(boolean visible, boolean restart) { + boolean result = super.setVisible(visible, restart); + if (visible) { + reschedule(); + } else { + unscheduleSelf(this); + } + return result; + } + + private void reschedule() { + if (!isVisible()) { + return; + } + + unscheduleSelf(this); + final long upTime = SystemClock.uptimeMillis(); + final long step = TICK_MS; /* tick every 200 ms */ + scheduleSelf(this, upTime - ((upTime % step)) + step); + } + + @Override + public ConstantState getConstantState() { + return new ClockConstantState(mInfo, isDisabled()); + } + + private static class ClockConstantState extends FastBitmapConstantState { + + private final ClockBitmapInfo mInfo; + + ClockConstantState(ClockBitmapInfo info, boolean isDisabled) { + super(info.icon, info.color, isDisabled); + mInfo = info; + } + + @Override + public FastBitmapDrawable newDrawable() { + ClockIconDrawable drawable = new ClockIconDrawable(mInfo); + drawable.setIsDisabled(mIsDisabled); + return drawable; + } + } + } +} diff --git a/PermissionController/iconloaderlib/src/com/android/launcher3/icons/ColorExtractor.java b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/ColorExtractor.java new file mode 100644 index 000000000..87bda825c --- /dev/null +++ b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/ColorExtractor.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.icons; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.util.SparseArray; +import java.util.Arrays; + +/** + * Utility class for extracting colors from a bitmap. + */ +public class ColorExtractor { + + private final int NUM_SAMPLES = 20; + private final float[] mTmpHsv = new float[3]; + private final float[] mTmpHueScoreHistogram = new float[360]; + private final int[] mTmpPixels = new int[NUM_SAMPLES]; + private final SparseArray<Float> mTmpRgbScores = new SparseArray<>(); + + /** + * This picks a dominant color, looking for high-saturation, high-value, repeated hues. + * @param bitmap The bitmap to scan + */ + public int findDominantColorByHue(Bitmap bitmap) { + return findDominantColorByHue(bitmap, NUM_SAMPLES); + } + + /** + * This picks a dominant color, looking for high-saturation, high-value, repeated hues. + * @param bitmap The bitmap to scan + */ + public int findDominantColorByHue(Bitmap bitmap, int samples) { + final int height = bitmap.getHeight(); + final int width = bitmap.getWidth(); + int sampleStride = (int) Math.sqrt((height * width) / samples); + if (sampleStride < 1) { + sampleStride = 1; + } + + // This is an out-param, for getting the hsv values for an rgb + float[] hsv = mTmpHsv; + Arrays.fill(hsv, 0); + + // First get the best hue, by creating a histogram over 360 hue buckets, + // where each pixel contributes a score weighted by saturation, value, and alpha. + float[] hueScoreHistogram = mTmpHueScoreHistogram; + Arrays.fill(hueScoreHistogram, 0); + float highScore = -1; + int bestHue = -1; + + int[] pixels = mTmpPixels; + Arrays.fill(pixels, 0); + int pixelCount = 0; + + for (int y = 0; y < height; y += sampleStride) { + for (int x = 0; x < width; x += sampleStride) { + int argb = bitmap.getPixel(x, y); + int alpha = 0xFF & (argb >> 24); + if (alpha < 0x80) { + // Drop mostly-transparent pixels. + continue; + } + // Remove the alpha channel. + int rgb = argb | 0xFF000000; + Color.colorToHSV(rgb, hsv); + // Bucket colors by the 360 integer hues. + int hue = (int) hsv[0]; + if (hue < 0 || hue >= hueScoreHistogram.length) { + // Defensively avoid array bounds violations. + continue; + } + if (pixelCount < samples) { + pixels[pixelCount++] = rgb; + } + float score = hsv[1] * hsv[2]; + hueScoreHistogram[hue] += score; + if (hueScoreHistogram[hue] > highScore) { + highScore = hueScoreHistogram[hue]; + bestHue = hue; + } + } + } + + SparseArray<Float> rgbScores = mTmpRgbScores; + rgbScores.clear(); + int bestColor = 0xff000000; + highScore = -1; + // Go back over the RGB colors that match the winning hue, + // creating a histogram of weighted s*v scores, for up to 100*100 [s,v] buckets. + // The highest-scoring RGB color wins. + for (int i = 0; i < pixelCount; i++) { + int rgb = pixels[i]; + Color.colorToHSV(rgb, hsv); + int hue = (int) hsv[0]; + if (hue == bestHue) { + float s = hsv[1]; + float v = hsv[2]; + int bucket = (int) (s * 100) + (int) (v * 10000); + // Score by cumulative saturation * value. + float score = s * v; + Float oldTotal = rgbScores.get(bucket); + float newTotal = oldTotal == null ? score : oldTotal + score; + rgbScores.put(bucket, newTotal); + if (newTotal > highScore) { + highScore = newTotal; + // All the colors in the winning bucket are very similar. Last in wins. + bestColor = rgb; + } + } + } + return bestColor; + } +} diff --git a/PermissionController/iconloaderlib/src/com/android/launcher3/icons/DotRenderer.java b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/DotRenderer.java new file mode 100644 index 000000000..97a0fd3ff --- /dev/null +++ b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/DotRenderer.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.icons; + +import static android.graphics.Paint.ANTI_ALIAS_FLAG; +import static android.graphics.Paint.FILTER_BITMAP_FLAG; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PathMeasure; +import android.graphics.Rect; +import android.graphics.RectF; +import android.util.Log; +import android.view.ViewDebug; + +/** + * Used to draw a notification dot on top of an icon. + */ +public class DotRenderer { + + private static final String TAG = "DotRenderer"; + + // The dot size is defined as a percentage of the app icon size. + private static final float SIZE_PERCENTAGE = 0.228f; + + private final float mCircleRadius; + private final Paint mCirclePaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG); + + private final Bitmap mBackgroundWithShadow; + private final float mBitmapOffset; + + // Stores the center x and y position as a percentage (0 to 1) of the icon size + private final float[] mRightDotPosition; + private final float[] mLeftDotPosition; + + public DotRenderer(int iconSizePx, Path iconShapePath, int pathSize) { + int size = Math.round(SIZE_PERCENTAGE * iconSizePx); + ShadowGenerator.Builder builder = new ShadowGenerator.Builder(Color.TRANSPARENT); + builder.ambientShadowAlpha = 88; + mBackgroundWithShadow = builder.setupBlurForSize(size).createPill(size, size); + mCircleRadius = builder.radius; + + mBitmapOffset = -mBackgroundWithShadow.getHeight() * 0.5f; // Same as width. + + // Find the points on the path that are closest to the top left and right corners. + mLeftDotPosition = getPathPoint(iconShapePath, pathSize, -1); + mRightDotPosition = getPathPoint(iconShapePath, pathSize, 1); + } + + private static float[] getPathPoint(Path path, float size, float direction) { + float halfSize = size / 2; + // Small delta so that we don't get a zero size triangle + float delta = 1; + + float x = halfSize + direction * halfSize; + Path trianglePath = new Path(); + trianglePath.moveTo(halfSize, halfSize); + trianglePath.lineTo(x + delta * direction, 0); + trianglePath.lineTo(x, -delta); + trianglePath.close(); + + trianglePath.op(path, Path.Op.INTERSECT); + float[] pos = new float[2]; + new PathMeasure(trianglePath, false).getPosTan(0, pos, null); + + pos[0] = pos[0] / size; + pos[1] = pos[1] / size; + return pos; + } + + public float[] getLeftDotPosition() { + return mLeftDotPosition; + } + + public float[] getRightDotPosition() { + return mRightDotPosition; + } + + /** + * Draw a circle on top of the canvas according to the given params. + */ + public void draw(Canvas canvas, DrawParams params) { + if (params == null) { + Log.e(TAG, "Invalid null argument(s) passed in call to draw."); + return; + } + canvas.save(); + + Rect iconBounds = params.iconBounds; + float[] dotPosition = params.leftAlign ? mLeftDotPosition : mRightDotPosition; + float dotCenterX = iconBounds.left + iconBounds.width() * dotPosition[0]; + float dotCenterY = iconBounds.top + iconBounds.height() * dotPosition[1]; + + // Ensure dot fits entirely in canvas clip bounds. + Rect canvasBounds = canvas.getClipBounds(); + float offsetX = params.leftAlign + ? Math.max(0, canvasBounds.left - (dotCenterX + mBitmapOffset)) + : Math.min(0, canvasBounds.right - (dotCenterX - mBitmapOffset)); + float offsetY = Math.max(0, canvasBounds.top - (dotCenterY + mBitmapOffset)); + + // We draw the dot relative to its center. + canvas.translate(dotCenterX + offsetX, dotCenterY + offsetY); + canvas.scale(params.scale, params.scale); + + mCirclePaint.setColor(Color.BLACK); + canvas.drawBitmap(mBackgroundWithShadow, mBitmapOffset, mBitmapOffset, mCirclePaint); + mCirclePaint.setColor(params.color); + canvas.drawCircle(0, 0, mCircleRadius, mCirclePaint); + canvas.restore(); + } + + public static class DrawParams { + /** The color (possibly based on the icon) to use for the dot. */ + @ViewDebug.ExportedProperty(category = "notification dot", formatToHexString = true) + public int color; + /** The bounds of the icon that the dot is drawn on top of. */ + @ViewDebug.ExportedProperty(category = "notification dot") + public Rect iconBounds = new Rect(); + /** The progress of the animation, from 0 to 1. */ + @ViewDebug.ExportedProperty(category = "notification dot") + public float scale; + /** Whether the dot should align to the top left of the icon rather than the top right. */ + @ViewDebug.ExportedProperty(category = "notification dot") + public boolean leftAlign; + } +} diff --git a/PermissionController/iconloaderlib/src/com/android/launcher3/icons/FastBitmapDrawable.java b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/FastBitmapDrawable.java new file mode 100644 index 000000000..4aa284618 --- /dev/null +++ b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/FastBitmapDrawable.java @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.icons; + +import android.animation.ObjectAnimator; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.Property; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import androidx.annotation.Nullable; + +public class FastBitmapDrawable extends Drawable { + + private static final Interpolator ACCEL = new AccelerateInterpolator(); + private static final Interpolator DEACCEL = new DecelerateInterpolator(); + + private static final float PRESSED_SCALE = 1.1f; + + private static final float DISABLED_DESATURATION = 1f; + private static final float DISABLED_BRIGHTNESS = 0.5f; + + public static final int CLICK_FEEDBACK_DURATION = 200; + + private static ColorFilter sDisabledFColorFilter; + + protected final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG); + protected Bitmap mBitmap; + protected final int mIconColor; + + @Nullable private ColorFilter mColorFilter; + + private boolean mIsPressed; + protected boolean mIsDisabled; + float mDisabledAlpha = 1f; + + // Animator and properties for the fast bitmap drawable's scale + private static final Property<FastBitmapDrawable, Float> SCALE + = new Property<FastBitmapDrawable, Float>(Float.TYPE, "scale") { + @Override + public Float get(FastBitmapDrawable fastBitmapDrawable) { + return fastBitmapDrawable.mScale; + } + + @Override + public void set(FastBitmapDrawable fastBitmapDrawable, Float value) { + fastBitmapDrawable.mScale = value; + fastBitmapDrawable.invalidateSelf(); + } + }; + private ObjectAnimator mScaleAnimation; + private float mScale = 1; + + private int mAlpha = 255; + + public FastBitmapDrawable(Bitmap b) { + this(b, Color.TRANSPARENT); + } + + public FastBitmapDrawable(BitmapInfo info) { + this(info.icon, info.color); + } + + protected FastBitmapDrawable(Bitmap b, int iconColor) { + this(b, iconColor, false); + } + + protected FastBitmapDrawable(Bitmap b, int iconColor, boolean isDisabled) { + mBitmap = b; + mIconColor = iconColor; + setFilterBitmap(true); + setIsDisabled(isDisabled); + } + + @Override + public final void draw(Canvas canvas) { + if (mScale != 1f) { + int count = canvas.save(); + Rect bounds = getBounds(); + canvas.scale(mScale, mScale, bounds.exactCenterX(), bounds.exactCenterY()); + drawInternal(canvas, bounds); + canvas.restoreToCount(count); + } else { + drawInternal(canvas, getBounds()); + } + } + + protected void drawInternal(Canvas canvas, Rect bounds) { + canvas.drawBitmap(mBitmap, null, bounds, mPaint); + } + + /** + * Returns the primary icon color + */ + public int getIconColor() { + return mIconColor; + } + + /** + * Returns if this represents a themed icon + */ + public boolean isThemed() { + return false; + } + + @Override + public void setColorFilter(ColorFilter cf) { + mColorFilter = cf; + updateFilter(); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public void setAlpha(int alpha) { + if (mAlpha != alpha) { + mAlpha = alpha; + mPaint.setAlpha(alpha); + invalidateSelf(); + } + } + + @Override + public void setFilterBitmap(boolean filterBitmap) { + mPaint.setFilterBitmap(filterBitmap); + mPaint.setAntiAlias(filterBitmap); + } + + @Override + public int getAlpha() { + return mAlpha; + } + + public void resetScale() { + if (mScaleAnimation != null) { + mScaleAnimation.cancel(); + mScaleAnimation = null; + } + mScale = 1; + invalidateSelf(); + } + + public float getAnimatedScale() { + return mScaleAnimation == null ? 1 : mScale; + } + + @Override + public int getIntrinsicWidth() { + return mBitmap.getWidth(); + } + + @Override + public int getIntrinsicHeight() { + return mBitmap.getHeight(); + } + + @Override + public int getMinimumWidth() { + return getBounds().width(); + } + + @Override + public int getMinimumHeight() { + return getBounds().height(); + } + + @Override + public boolean isStateful() { + return true; + } + + @Override + public ColorFilter getColorFilter() { + return mPaint.getColorFilter(); + } + + @Override + protected boolean onStateChange(int[] state) { + boolean isPressed = false; + for (int s : state) { + if (s == android.R.attr.state_pressed) { + isPressed = true; + break; + } + } + if (mIsPressed != isPressed) { + mIsPressed = isPressed; + + if (mScaleAnimation != null) { + mScaleAnimation.cancel(); + mScaleAnimation = null; + } + + if (mIsPressed) { + // Animate when going to pressed state + mScaleAnimation = ObjectAnimator.ofFloat(this, SCALE, PRESSED_SCALE); + mScaleAnimation.setDuration(CLICK_FEEDBACK_DURATION); + mScaleAnimation.setInterpolator(ACCEL); + mScaleAnimation.start(); + } else { + if (isVisible()) { + mScaleAnimation = ObjectAnimator.ofFloat(this, SCALE, 1f); + mScaleAnimation.setDuration(CLICK_FEEDBACK_DURATION); + mScaleAnimation.setInterpolator(DEACCEL); + mScaleAnimation.start(); + } else { + mScale = 1f; + invalidateSelf(); + } + } + return true; + } + return false; + } + + public void setIsDisabled(boolean isDisabled) { + if (mIsDisabled != isDisabled) { + mIsDisabled = isDisabled; + updateFilter(); + } + } + + protected boolean isDisabled() { + return mIsDisabled; + } + + private ColorFilter getDisabledColorFilter() { + if (sDisabledFColorFilter == null) { + sDisabledFColorFilter = getDisabledFColorFilter(mDisabledAlpha); + } + return sDisabledFColorFilter; + } + + /** + * Updates the paint to reflect the current brightness and saturation. + */ + protected void updateFilter() { + mPaint.setColorFilter(mIsDisabled ? getDisabledColorFilter() : mColorFilter); + invalidateSelf(); + } + + @Override + public ConstantState getConstantState() { + return new FastBitmapConstantState(mBitmap, mIconColor, mIsDisabled); + } + + public static ColorFilter getDisabledFColorFilter(float disabledAlpha) { + ColorMatrix tempBrightnessMatrix = new ColorMatrix(); + ColorMatrix tempFilterMatrix = new ColorMatrix(); + + tempFilterMatrix.setSaturation(1f - DISABLED_DESATURATION); + float scale = 1 - DISABLED_BRIGHTNESS; + int brightnessI = (int) (255 * DISABLED_BRIGHTNESS); + float[] mat = tempBrightnessMatrix.getArray(); + mat[0] = scale; + mat[6] = scale; + mat[12] = scale; + mat[4] = brightnessI; + mat[9] = brightnessI; + mat[14] = brightnessI; + mat[18] = disabledAlpha; + tempFilterMatrix.preConcat(tempBrightnessMatrix); + return new ColorMatrixColorFilter(tempBrightnessMatrix); + } + + protected static class FastBitmapConstantState extends ConstantState { + protected final Bitmap mBitmap; + protected final int mIconColor; + protected final boolean mIsDisabled; + + public FastBitmapConstantState(Bitmap bitmap, int color, boolean isDisabled) { + mBitmap = bitmap; + mIconColor = color; + mIsDisabled = isDisabled; + } + + @Override + public FastBitmapDrawable newDrawable() { + return new FastBitmapDrawable(mBitmap, mIconColor, mIsDisabled); + } + + @Override + public int getChangingConfigurations() { + return 0; + } + } +} diff --git a/PermissionController/iconloaderlib/src/com/android/launcher3/icons/FixedScaleDrawable.java b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/FixedScaleDrawable.java new file mode 100644 index 000000000..516965ec2 --- /dev/null +++ b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/FixedScaleDrawable.java @@ -0,0 +1,53 @@ +package com.android.launcher3.icons; + +import android.content.res.Resources; +import android.content.res.Resources.Theme; +import android.graphics.Canvas; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.DrawableWrapper; +import android.util.AttributeSet; + +import org.xmlpull.v1.XmlPullParser; + +/** + * Extension of {@link DrawableWrapper} which scales the child drawables by a fixed amount. + */ +public class FixedScaleDrawable extends DrawableWrapper { + + // TODO b/33553066 use the constant defined in MaskableIconDrawable + private static final float LEGACY_ICON_SCALE = .7f * .6667f; + private float mScaleX, mScaleY; + + public FixedScaleDrawable() { + super(new ColorDrawable()); + mScaleX = LEGACY_ICON_SCALE; + mScaleY = LEGACY_ICON_SCALE; + } + + @Override + public void draw(Canvas canvas) { + int saveCount = canvas.save(); + canvas.scale(mScaleX, mScaleY, + getBounds().exactCenterX(), getBounds().exactCenterY()); + super.draw(canvas); + canvas.restoreToCount(saveCount); + } + + @Override + public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) { } + + @Override + public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) { } + + public void setScale(float scale) { + float h = getIntrinsicHeight(); + float w = getIntrinsicWidth(); + mScaleX = scale * LEGACY_ICON_SCALE; + mScaleY = scale * LEGACY_ICON_SCALE; + if (h > w && w > 0) { + mScaleX *= w / h; + } else if (w > h && h > 0) { + mScaleY *= h / w; + } + } +} diff --git a/PermissionController/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java new file mode 100644 index 000000000..17b001642 --- /dev/null +++ b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.icons; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.RegionIterator; +import android.graphics.drawable.AdaptiveIconDrawable; +import android.graphics.drawable.ColorDrawable; +import android.util.Log; + +import androidx.annotation.ColorInt; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class GraphicsUtils { + + private static final String TAG = "GraphicsUtils"; + + public static Runnable sOnNewBitmapRunnable = () -> { }; + + /** + * Set the alpha component of {@code color} to be {@code alpha}. Unlike the support lib version, + * it bounds the alpha in valid range instead of throwing an exception to allow for safer + * interpolation of color animations + */ + @ColorInt + public static int setColorAlphaBound(int color, int alpha) { + if (alpha < 0) { + alpha = 0; + } else if (alpha > 255) { + alpha = 255; + } + return (color & 0x00ffffff) | (alpha << 24); + } + + /** + * Compresses the bitmap to a byte array for serialization. + */ + public static byte[] flattenBitmap(Bitmap bitmap) { + ByteArrayOutputStream out = new ByteArrayOutputStream(getExpectedBitmapSize(bitmap)); + try { + bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); + out.flush(); + out.close(); + return out.toByteArray(); + } catch (IOException e) { + Log.w(TAG, "Could not write bitmap"); + return null; + } + } + + /** + * Try go guesstimate how much space the icon will take when serialized to avoid unnecessary + * allocations/copies during the write (4 bytes per pixel). + */ + static int getExpectedBitmapSize(Bitmap bitmap) { + return bitmap.getWidth() * bitmap.getHeight() * 4; + } + + public static int getArea(Region r) { + RegionIterator itr = new RegionIterator(r); + int area = 0; + Rect tempRect = new Rect(); + while (itr.next(tempRect)) { + area += tempRect.width() * tempRect.height(); + } + return area; + } + + /** + * Utility method to track new bitmap creation + */ + public static void noteNewBitmapCreated() { + sOnNewBitmapRunnable.run(); + } + + + /** + * Returns the default path to be used by an icon + */ + public static Path getShapePath(int size) { + AdaptiveIconDrawable drawable = new AdaptiveIconDrawable( + new ColorDrawable(Color.BLACK), new ColorDrawable(Color.BLACK)); + drawable.setBounds(0, 0, size, size); + return new Path(drawable.getIconMask()); + } + + /** + * Returns the color associated with the attribute + */ + public static int getAttrColor(Context context, int attr) { + TypedArray ta = context.obtainStyledAttributes(new int[]{attr}); + int colorAccent = ta.getColor(0, 0); + ta.recycle(); + return colorAccent; + } + + /** + * Returns the alpha corresponding to the theme attribute {@param attr} + */ + public static float getFloat(Context context, int attr, float defValue) { + TypedArray ta = context.obtainStyledAttributes(new int[]{attr}); + float value = ta.getFloat(0, defValue); + ta.recycle(); + return value; + } +} diff --git a/PermissionController/iconloaderlib/src/com/android/launcher3/icons/IconNormalizer.java b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/IconNormalizer.java new file mode 100644 index 000000000..de39e79fe --- /dev/null +++ b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/IconNormalizer.java @@ -0,0 +1,411 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.icons; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; +import android.graphics.drawable.AdaptiveIconDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.Log; + +import java.nio.ByteBuffer; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class IconNormalizer { + + private static final String TAG = "IconNormalizer"; + private static final boolean DEBUG = false; + // Ratio of icon visible area to full icon size for a square shaped icon + private static final float MAX_SQUARE_AREA_FACTOR = 375.0f / 576; + // Ratio of icon visible area to full icon size for a circular shaped icon + private static final float MAX_CIRCLE_AREA_FACTOR = 380.0f / 576; + + private static final float CIRCLE_AREA_BY_RECT = (float) Math.PI / 4; + + // Slope used to calculate icon visible area to full icon size for any generic shaped icon. + private static final float LINEAR_SCALE_SLOPE = + (MAX_CIRCLE_AREA_FACTOR - MAX_SQUARE_AREA_FACTOR) / (1 - CIRCLE_AREA_BY_RECT); + + private static final int MIN_VISIBLE_ALPHA = 40; + + // Shape detection related constants + private static final float BOUND_RATIO_MARGIN = .05f; + private static final float PIXEL_DIFF_PERCENTAGE_THRESHOLD = 0.005f; + private static final float SCALE_NOT_INITIALIZED = 0; + + // Ratio of the diameter of an normalized circular icon to the actual icon size. + public static final float ICON_VISIBLE_AREA_FACTOR = 0.92f; + + private final int mMaxSize; + private final Bitmap mBitmap; + private final Canvas mCanvas; + private final Paint mPaintMaskShape; + private final Paint mPaintMaskShapeOutline; + private final byte[] mPixels; + + private final RectF mAdaptiveIconBounds; + private float mAdaptiveIconScale; + + private boolean mEnableShapeDetection; + + // for each y, stores the position of the leftmost x and the rightmost x + private final float[] mLeftBorder; + private final float[] mRightBorder; + private final Rect mBounds; + private final Path mShapePath; + private final Matrix mMatrix; + + /** package private **/ + IconNormalizer(Context context, int iconBitmapSize, boolean shapeDetection) { + // Use twice the icon size as maximum size to avoid scaling down twice. + mMaxSize = iconBitmapSize * 2; + mBitmap = Bitmap.createBitmap(mMaxSize, mMaxSize, Bitmap.Config.ALPHA_8); + mCanvas = new Canvas(mBitmap); + mPixels = new byte[mMaxSize * mMaxSize]; + mLeftBorder = new float[mMaxSize]; + mRightBorder = new float[mMaxSize]; + mBounds = new Rect(); + mAdaptiveIconBounds = new RectF(); + + mPaintMaskShape = new Paint(); + mPaintMaskShape.setColor(Color.RED); + mPaintMaskShape.setStyle(Paint.Style.FILL); + mPaintMaskShape.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XOR)); + + mPaintMaskShapeOutline = new Paint(); + mPaintMaskShapeOutline.setStrokeWidth( + 2 * context.getResources().getDisplayMetrics().density); + mPaintMaskShapeOutline.setStyle(Paint.Style.STROKE); + mPaintMaskShapeOutline.setColor(Color.BLACK); + mPaintMaskShapeOutline.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + + mShapePath = new Path(); + mMatrix = new Matrix(); + mAdaptiveIconScale = SCALE_NOT_INITIALIZED; + mEnableShapeDetection = shapeDetection; + } + + private static float getScale(float hullArea, float boundingArea, float fullArea) { + float hullByRect = hullArea / boundingArea; + float scaleRequired; + if (hullByRect < CIRCLE_AREA_BY_RECT) { + scaleRequired = MAX_CIRCLE_AREA_FACTOR; + } else { + scaleRequired = MAX_SQUARE_AREA_FACTOR + LINEAR_SCALE_SLOPE * (1 - hullByRect); + } + + float areaScale = hullArea / fullArea; + // Use sqrt of the final ratio as the images is scaled across both width and height. + return areaScale > scaleRequired ? (float) Math.sqrt(scaleRequired / areaScale) : 1; + } + + /** + * @param d Should be AdaptiveIconDrawable + * @param size Canvas size to use + */ + @TargetApi(Build.VERSION_CODES.O) + public static float normalizeAdaptiveIcon(Drawable d, int size, @Nullable RectF outBounds) { + Rect tmpBounds = new Rect(d.getBounds()); + d.setBounds(0, 0, size, size); + + Path path = ((AdaptiveIconDrawable) d).getIconMask(); + Region region = new Region(); + region.setPath(path, new Region(0, 0, size, size)); + + Rect hullBounds = region.getBounds(); + int hullArea = GraphicsUtils.getArea(region); + + if (outBounds != null) { + float sizeF = size; + outBounds.set( + hullBounds.left / sizeF, + hullBounds.top / sizeF, + 1 - (hullBounds.right / sizeF), + 1 - (hullBounds.bottom / sizeF)); + } + d.setBounds(tmpBounds); + return getScale(hullArea, hullArea, size * size); + } + + /** + * Returns if the shape of the icon is same as the path. + * For this method to work, the shape path bounds should be in [0,1]x[0,1] bounds. + */ + private boolean isShape(Path maskPath) { + // Condition1: + // If width and height of the path not close to a square, then the icon shape is + // not same as the mask shape. + float iconRatio = ((float) mBounds.width()) / mBounds.height(); + if (Math.abs(iconRatio - 1) > BOUND_RATIO_MARGIN) { + if (DEBUG) { + Log.d(TAG, "Not same as mask shape because width != height. " + iconRatio); + } + return false; + } + + // Condition 2: + // Actual icon (white) and the fitted shape (e.g., circle)(red) XOR operation + // should generate transparent image, if the actual icon is equivalent to the shape. + + // Fit the shape within the icon's bounding box + mMatrix.reset(); + mMatrix.setScale(mBounds.width(), mBounds.height()); + mMatrix.postTranslate(mBounds.left, mBounds.top); + maskPath.transform(mMatrix, mShapePath); + + // XOR operation + mCanvas.drawPath(mShapePath, mPaintMaskShape); + + // DST_OUT operation around the mask path outline + mCanvas.drawPath(mShapePath, mPaintMaskShapeOutline); + + // Check if the result is almost transparent + return isTransparentBitmap(); + } + + /** + * Used to determine if certain the bitmap is transparent. + */ + private boolean isTransparentBitmap() { + ByteBuffer buffer = ByteBuffer.wrap(mPixels); + buffer.rewind(); + mBitmap.copyPixelsToBuffer(buffer); + + int y = mBounds.top; + // buffer position + int index = y * mMaxSize; + // buffer shift after every row, width of buffer = mMaxSize + int rowSizeDiff = mMaxSize - mBounds.right; + + int sum = 0; + for (; y < mBounds.bottom; y++) { + index += mBounds.left; + for (int x = mBounds.left; x < mBounds.right; x++) { + if ((mPixels[index] & 0xFF) > MIN_VISIBLE_ALPHA) { + sum++; + } + index++; + } + index += rowSizeDiff; + } + + float percentageDiffPixels = ((float) sum) / (mBounds.width() * mBounds.height()); + return percentageDiffPixels < PIXEL_DIFF_PERCENTAGE_THRESHOLD; + } + + /** + * Returns the amount by which the {@param d} should be scaled (in both dimensions) so that it + * matches the design guidelines for a launcher icon. + * + * We first calculate the convex hull of the visible portion of the icon. + * This hull then compared with the bounding rectangle of the hull to find how closely it + * resembles a circle and a square, by comparing the ratio of the areas. Note that this is not an + * ideal solution but it gives satisfactory result without affecting the performance. + * + * This closeness is used to determine the ratio of hull area to the full icon size. + * Refer {@link #MAX_CIRCLE_AREA_FACTOR} and {@link #MAX_SQUARE_AREA_FACTOR} + * + * @param outBounds optional rect to receive the fraction distance from each edge. + */ + public synchronized float getScale(@NonNull Drawable d, @Nullable RectF outBounds, + @Nullable Path path, @Nullable boolean[] outMaskShape) { + if (BaseIconFactory.ATLEAST_OREO && d instanceof AdaptiveIconDrawable) { + if (mAdaptiveIconScale == SCALE_NOT_INITIALIZED) { + mAdaptiveIconScale = normalizeAdaptiveIcon(d, mMaxSize, mAdaptiveIconBounds); + } + if (outBounds != null) { + outBounds.set(mAdaptiveIconBounds); + } + return mAdaptiveIconScale; + } + int width = d.getIntrinsicWidth(); + int height = d.getIntrinsicHeight(); + if (width <= 0 || height <= 0) { + width = width <= 0 || width > mMaxSize ? mMaxSize : width; + height = height <= 0 || height > mMaxSize ? mMaxSize : height; + } else if (width > mMaxSize || height > mMaxSize) { + int max = Math.max(width, height); + width = mMaxSize * width / max; + height = mMaxSize * height / max; + } + + mBitmap.eraseColor(Color.TRANSPARENT); + d.setBounds(0, 0, width, height); + d.draw(mCanvas); + + ByteBuffer buffer = ByteBuffer.wrap(mPixels); + buffer.rewind(); + mBitmap.copyPixelsToBuffer(buffer); + + // Overall bounds of the visible icon. + int topY = -1; + int bottomY = -1; + int leftX = mMaxSize + 1; + int rightX = -1; + + // Create border by going through all pixels one row at a time and for each row find + // the first and the last non-transparent pixel. Set those values to mLeftBorder and + // mRightBorder and use -1 if there are no visible pixel in the row. + + // buffer position + int index = 0; + // buffer shift after every row, width of buffer = mMaxSize + int rowSizeDiff = mMaxSize - width; + // first and last position for any row. + int firstX, lastX; + + for (int y = 0; y < height; y++) { + firstX = lastX = -1; + for (int x = 0; x < width; x++) { + if ((mPixels[index] & 0xFF) > MIN_VISIBLE_ALPHA) { + if (firstX == -1) { + firstX = x; + } + lastX = x; + } + index++; + } + index += rowSizeDiff; + + mLeftBorder[y] = firstX; + mRightBorder[y] = lastX; + + // If there is at least one visible pixel, update the overall bounds. + if (firstX != -1) { + bottomY = y; + if (topY == -1) { + topY = y; + } + + leftX = Math.min(leftX, firstX); + rightX = Math.max(rightX, lastX); + } + } + + if (topY == -1 || rightX == -1) { + // No valid pixels found. Do not scale. + return 1; + } + + convertToConvexArray(mLeftBorder, 1, topY, bottomY); + convertToConvexArray(mRightBorder, -1, topY, bottomY); + + // Area of the convex hull + float area = 0; + for (int y = 0; y < height; y++) { + if (mLeftBorder[y] <= -1) { + continue; + } + area += mRightBorder[y] - mLeftBorder[y] + 1; + } + + mBounds.left = leftX; + mBounds.right = rightX; + + mBounds.top = topY; + mBounds.bottom = bottomY; + + if (outBounds != null) { + outBounds.set(((float) mBounds.left) / width, ((float) mBounds.top) / height, + 1 - ((float) mBounds.right) / width, + 1 - ((float) mBounds.bottom) / height); + } + if (outMaskShape != null && mEnableShapeDetection && outMaskShape.length > 0) { + outMaskShape[0] = isShape(path); + } + // Area of the rectangle required to fit the convex hull + float rectArea = (bottomY + 1 - topY) * (rightX + 1 - leftX); + return getScale(area, rectArea, width * height); + } + + /** + * Modifies {@param xCoordinates} to represent a convex border. Fills in all missing values + * (except on either ends) with appropriate values. + * @param xCoordinates map of x coordinate per y. + * @param direction 1 for left border and -1 for right border. + * @param topY the first Y position (inclusive) with a valid value. + * @param bottomY the last Y position (inclusive) with a valid value. + */ + private static void convertToConvexArray( + float[] xCoordinates, int direction, int topY, int bottomY) { + int total = xCoordinates.length; + // The tangent at each pixel. + float[] angles = new float[total - 1]; + + int first = topY; // First valid y coordinate + int last = -1; // Last valid y coordinate which didn't have a missing value + + float lastAngle = Float.MAX_VALUE; + + for (int i = topY + 1; i <= bottomY; i++) { + if (xCoordinates[i] <= -1) { + continue; + } + int start; + + if (lastAngle == Float.MAX_VALUE) { + start = first; + } else { + float currentAngle = (xCoordinates[i] - xCoordinates[last]) / (i - last); + start = last; + // If this position creates a concave angle, keep moving up until we find a + // position which creates a convex angle. + if ((currentAngle - lastAngle) * direction < 0) { + while (start > first) { + start --; + currentAngle = (xCoordinates[i] - xCoordinates[start]) / (i - start); + if ((currentAngle - angles[start]) * direction >= 0) { + break; + } + } + } + } + + // Reset from last check + lastAngle = (xCoordinates[i] - xCoordinates[start]) / (i - start); + // Update all the points from start. + for (int j = start; j < i; j++) { + angles[j] = lastAngle; + xCoordinates[j] = xCoordinates[start] + lastAngle * (j - start); + } + last = i; + } + } + + /** + * @return The diameter of the normalized circle that fits inside of the square (size x size). + */ + public static int getNormalizedCircleSize(int size) { + float area = size * size * MAX_CIRCLE_AREA_FACTOR; + return (int) Math.round(Math.sqrt((4 * area) / Math.PI)); + } +} diff --git a/PermissionController/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java new file mode 100644 index 000000000..449c0daa5 --- /dev/null +++ b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java @@ -0,0 +1,377 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.icons; + +import static android.content.Intent.ACTION_DATE_CHANGED; +import static android.content.Intent.ACTION_TIMEZONE_CHANGED; +import static android.content.Intent.ACTION_TIME_CHANGED; +import static android.content.res.Resources.ID_NULL; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ActivityInfo; +import android.content.pm.LauncherActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Handler; +import android.os.PatternMatcher; +import android.os.Process; +import android.os.UserHandle; +import android.os.UserManager; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Log; + +import com.android.launcher3.icons.ThemedIconDrawable.ThemeData; +import com.android.launcher3.util.SafeCloseable; + +import org.xmlpull.v1.XmlPullParser; + +import java.util.Calendar; +import java.util.Collections; +import java.util.Map; +import java.util.function.Supplier; + +/** + * Class to handle icon loading from different packages + */ +public class IconProvider { + + private final String ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED"; + private static final int CONFIG_ICON_MASK_RES_ID = Resources.getSystem().getIdentifier( + "config_icon_mask", "string", "android"); + + private static final String TAG_ICON = "icon"; + private static final String ATTR_PACKAGE = "package"; + private static final String ATTR_DRAWABLE = "drawable"; + + private static final String TAG = "IconProvider"; + private static final boolean DEBUG = false; + + private static final String ICON_METADATA_KEY_PREFIX = ".dynamic_icons"; + + private static final String SYSTEM_STATE_SEPARATOR = " "; + private static final String THEMED_ICON_MAP_FILE = "grayscale_icon_map"; + + private static final Map<String, ThemeData> DISABLED_MAP = Collections.emptyMap(); + + private Map<String, ThemeData> mThemedIconMap; + + private final Context mContext; + private final ComponentName mCalendar; + private final ComponentName mClock; + + static final int ICON_TYPE_DEFAULT = 0; + static final int ICON_TYPE_CALENDAR = 1; + static final int ICON_TYPE_CLOCK = 2; + + public IconProvider(Context context) { + this(context, false); + } + + public IconProvider(Context context, boolean supportsIconTheme) { + mContext = context; + mCalendar = parseComponentOrNull(context, R.string.calendar_component_name); + mClock = parseComponentOrNull(context, R.string.clock_component_name); + if (!supportsIconTheme) { + // Initialize an empty map if theming is not supported + mThemedIconMap = DISABLED_MAP; + } + } + + /** + * Enables or disables icon theme support + */ + public void setIconThemeSupported(boolean isSupported) { + mThemedIconMap = isSupported ? null : DISABLED_MAP; + } + + /** + * Adds any modification to the provided systemState for dynamic icons. This system state + * is used by caches to check for icon invalidation. + */ + public String getSystemStateForPackage(String systemState, String packageName) { + if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) { + return systemState + SYSTEM_STATE_SEPARATOR + getDay(); + } else { + return systemState; + } + } + + /** + * Loads the icon for the provided LauncherActivityInfo + */ + public Drawable getIcon(LauncherActivityInfo info, int iconDpi) { + return getIconWithOverrides(info.getApplicationInfo().packageName, info.getUser(), iconDpi, + () -> info.getIcon(iconDpi)); + } + + /** + * Loads the icon for the provided activity info + */ + public Drawable getIcon(ActivityInfo info) { + return getIcon(info, mContext.getResources().getConfiguration().densityDpi); + } + + /** + * Loads the icon for the provided activity info + */ + public Drawable getIcon(ActivityInfo info, int iconDpi) { + return getIconWithOverrides(info.applicationInfo.packageName, + UserHandle.getUserHandleForUid(info.applicationInfo.uid), + iconDpi, () -> loadActivityInfoIcon(info, iconDpi)); + } + + private Drawable getIconWithOverrides(String packageName, UserHandle user, int iconDpi, + Supplier<Drawable> fallback) { + Drawable icon = null; + + int iconType = ICON_TYPE_DEFAULT; + if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) { + icon = loadCalendarDrawable(iconDpi); + iconType = ICON_TYPE_CALENDAR; + } else if (mClock != null + && mClock.getPackageName().equals(packageName) + && Process.myUserHandle().equals(user)) { + icon = loadClockDrawable(iconDpi); + iconType = ICON_TYPE_CLOCK; + } + if (icon == null) { + icon = fallback.get(); + iconType = ICON_TYPE_DEFAULT; + } + + ThemeData td = getThemedIconMap().get(packageName); + return td != null ? td.wrapDrawable(icon, iconType) : icon; + } + + private Drawable loadActivityInfoIcon(ActivityInfo ai, int density) { + final int iconRes = ai.getIconResource(); + Drawable icon = null; + // Get the preferred density icon from the app's resources + if (density != 0 && iconRes != 0) { + try { + final Resources resources = mContext.getPackageManager() + .getResourcesForApplication(ai.applicationInfo); + icon = resources.getDrawableForDensity(iconRes, density); + } catch (NameNotFoundException | Resources.NotFoundException exc) { } + } + // Get the default density icon + if (icon == null) { + icon = ai.loadIcon(mContext.getPackageManager()); + } + return icon; + } + + private Map<String, ThemeData> getThemedIconMap() { + if (mThemedIconMap != null) { + return mThemedIconMap; + } + ArrayMap<String, ThemeData> map = new ArrayMap<>(); + try { + Resources res = mContext.getResources(); + int resID = res.getIdentifier(THEMED_ICON_MAP_FILE, "xml", mContext.getPackageName()); + if (resID != 0) { + XmlResourceParser parser = res.getXml(resID); + final int depth = parser.getDepth(); + + int type; + + while ((type = parser.next()) != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT); + + while (((type = parser.next()) != XmlPullParser.END_TAG || + parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { + if (type != XmlPullParser.START_TAG) { + continue; + } + if (TAG_ICON.equals(parser.getName())) { + String pkg = parser.getAttributeValue(null, ATTR_PACKAGE); + int iconId = parser.getAttributeResourceValue(null, ATTR_DRAWABLE, 0); + if (iconId != 0 && !TextUtils.isEmpty(pkg)) { + map.put(pkg, new ThemeData(res, iconId)); + } + } + } + } + } catch (Exception e) { + Log.e(TAG, "Unable to parse icon map", e); + } + mThemedIconMap = map; + return mThemedIconMap; + } + + private Drawable loadCalendarDrawable(int iconDpi) { + PackageManager pm = mContext.getPackageManager(); + try { + final Bundle metadata = pm.getActivityInfo( + mCalendar, + PackageManager.GET_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA) + .metaData; + final Resources resources = pm.getResourcesForApplication(mCalendar.getPackageName()); + final int id = getDynamicIconId(metadata, resources); + if (id != ID_NULL) { + if (DEBUG) Log.d(TAG, "Got icon #" + id); + return resources.getDrawableForDensity(id, iconDpi, null /* theme */); + } + } catch (PackageManager.NameNotFoundException e) { + if (DEBUG) { + Log.d(TAG, "Could not get activityinfo or resources for package: " + + mCalendar.getPackageName()); + } + } + return null; + } + + private Drawable loadClockDrawable(int iconDpi) { + return ClockDrawableWrapper.forPackage(mContext, mClock.getPackageName(), iconDpi); + } + + /** + * @param metadata metadata of the default activity of Calendar + * @param resources from the Calendar package + * @return the resource id for today's Calendar icon; 0 if resources cannot be found. + */ + private int getDynamicIconId(Bundle metadata, Resources resources) { + if (metadata == null) { + return ID_NULL; + } + String key = mCalendar.getPackageName() + ICON_METADATA_KEY_PREFIX; + final int arrayId = metadata.getInt(key, ID_NULL); + if (arrayId == ID_NULL) { + return ID_NULL; + } + try { + return resources.obtainTypedArray(arrayId).getResourceId(getDay(), ID_NULL); + } catch (Resources.NotFoundException e) { + if (DEBUG) { + Log.d(TAG, "package defines '" + key + "' but corresponding array not found"); + } + return ID_NULL; + } + } + + /** + * @return Today's day of the month, zero-indexed. + */ + static int getDay() { + return Calendar.getInstance().get(Calendar.DAY_OF_MONTH) - 1; + } + + private static ComponentName parseComponentOrNull(Context context, int resId) { + String cn = context.getString(resId); + return TextUtils.isEmpty(cn) ? null : ComponentName.unflattenFromString(cn); + } + + /** + * Returns a string representation of the current system icon state + */ + public String getSystemIconState() { + return (CONFIG_ICON_MASK_RES_ID == ID_NULL + ? "" : mContext.getResources().getString(CONFIG_ICON_MASK_RES_ID)) + + (mThemedIconMap == DISABLED_MAP ? ",no-theme" : ",with-theme"); + } + + /** + * Registers a callback to listen for various system dependent icon changes. + */ + public SafeCloseable registerIconChangeListener(IconChangeListener listener, Handler handler) { + return new IconChangeReceiver(listener, handler); + } + + private class IconChangeReceiver extends BroadcastReceiver implements SafeCloseable { + + private final IconChangeListener mCallback; + private String mIconState; + + IconChangeReceiver(IconChangeListener callback, Handler handler) { + mCallback = callback; + mIconState = getSystemIconState(); + + + IntentFilter packageFilter = new IntentFilter(ACTION_OVERLAY_CHANGED); + packageFilter.addDataScheme("package"); + packageFilter.addDataSchemeSpecificPart("android", PatternMatcher.PATTERN_LITERAL); + mContext.registerReceiver(this, packageFilter, null, handler); + + if (mCalendar != null || mClock != null) { + final IntentFilter filter = new IntentFilter(ACTION_TIMEZONE_CHANGED); + if (mCalendar != null) { + filter.addAction(Intent.ACTION_TIME_CHANGED); + filter.addAction(ACTION_DATE_CHANGED); + } + mContext.registerReceiver(this, filter, null, handler); + } + } + + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case ACTION_TIMEZONE_CHANGED: + if (mClock != null) { + mCallback.onAppIconChanged(mClock.getPackageName(), Process.myUserHandle()); + } + // follow through + case ACTION_DATE_CHANGED: + case ACTION_TIME_CHANGED: + if (mCalendar != null) { + for (UserHandle user + : context.getSystemService(UserManager.class).getUserProfiles()) { + mCallback.onAppIconChanged(mCalendar.getPackageName(), user); + } + } + break; + case ACTION_OVERLAY_CHANGED: { + String newState = getSystemIconState(); + if (!mIconState.equals(newState)) { + mIconState = newState; + mCallback.onSystemIconStateChanged(mIconState); + } + break; + } + } + } + + @Override + public void close() { + mContext.unregisterReceiver(this); + } + } + + /** + * Listener for receiving icon changes + */ + public interface IconChangeListener { + + /** + * Called when the icon for a particular app changes + */ + void onAppIconChanged(String packageName, UserHandle user); + + /** + * Called when the global icon state changed, which can typically affect all icons + */ + void onSystemIconStateChanged(String iconState); + } +} diff --git a/PermissionController/iconloaderlib/src/com/android/launcher3/icons/PlaceHolderIconDrawable.java b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/PlaceHolderIconDrawable.java new file mode 100644 index 000000000..5f3343e31 --- /dev/null +++ b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/PlaceHolderIconDrawable.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.icons; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Path; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +import androidx.core.graphics.ColorUtils; + +/** + * Subclass which draws a placeholder icon when the actual icon is not yet loaded + */ +public class PlaceHolderIconDrawable extends FastBitmapDrawable { + + // Path in [0, 100] bounds. + private final Path mProgressPath; + + public PlaceHolderIconDrawable(BitmapInfo info, Context context) { + super(info); + + mProgressPath = GraphicsUtils.getShapePath(100); + mPaint.setColor(ColorUtils.compositeColors( + GraphicsUtils.getAttrColor(context, R.attr.loadingIconColor), info.color)); + } + + @Override + protected void drawInternal(Canvas canvas, Rect bounds) { + int saveCount = canvas.save(); + canvas.translate(bounds.left, bounds.top); + canvas.scale(bounds.width() / 100f, bounds.height() / 100f); + canvas.drawPath(mProgressPath, mPaint); + canvas.restoreToCount(saveCount); + } + + /** Updates this placeholder to {@code newIcon} with animation. */ + public void animateIconUpdate(Drawable newIcon) { + int placeholderColor = mPaint.getColor(); + int originalAlpha = Color.alpha(placeholderColor); + + ValueAnimator iconUpdateAnimation = ValueAnimator.ofInt(originalAlpha, 0); + iconUpdateAnimation.setDuration(375); + iconUpdateAnimation.addUpdateListener(valueAnimator -> { + int newAlpha = (int) valueAnimator.getAnimatedValue(); + int newColor = ColorUtils.setAlphaComponent(placeholderColor, newAlpha); + + newIcon.setColorFilter(new PorterDuffColorFilter(newColor, PorterDuff.Mode.SRC_ATOP)); + }); + iconUpdateAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + newIcon.setColorFilter(null); + } + }); + iconUpdateAnimation.start(); + } + +} diff --git a/PermissionController/iconloaderlib/src/com/android/launcher3/icons/RoundDrawableWrapper.java b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/RoundDrawableWrapper.java new file mode 100644 index 000000000..e569c1ea0 --- /dev/null +++ b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/RoundDrawableWrapper.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.icons; + +import android.graphics.Canvas; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.DrawableWrapper; + +/** + * A drawable which clips rounded corner around a child drawable + */ +public class RoundDrawableWrapper extends DrawableWrapper { + + private final RectF mTempRect = new RectF(); + private final Path mClipPath = new Path(); + private final float mRoundedCornersRadius; + + public RoundDrawableWrapper(Drawable dr, float radius) { + super(dr); + mRoundedCornersRadius = radius; + } + + @Override + protected void onBoundsChange(Rect bounds) { + mTempRect.set(getBounds()); + mClipPath.reset(); + mClipPath.addRoundRect(mTempRect, mRoundedCornersRadius, + mRoundedCornersRadius, Path.Direction.CCW); + super.onBoundsChange(bounds); + } + + @Override + public final void draw(Canvas canvas) { + int saveCount = canvas.save(); + canvas.clipPath(mClipPath); + super.draw(canvas); + canvas.restoreToCount(saveCount); + } +} diff --git a/PermissionController/iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java new file mode 100644 index 000000000..e24f353ad --- /dev/null +++ b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.icons; + +import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound; + +import android.graphics.Bitmap; +import android.graphics.BlurMaskFilter; +import android.graphics.BlurMaskFilter.Blur; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.RectF; + +/** + * Utility class to add shadows to bitmaps. + */ +public class ShadowGenerator { + + public static final boolean ENABLE_SHADOWS = true; + + public static final float BLUR_FACTOR = 1.5f/48; + + // Percent of actual icon size + public static final float KEY_SHADOW_DISTANCE = 1f/48; + private static final int KEY_SHADOW_ALPHA = 10; + // Percent of actual icon size + private static final float HALF_DISTANCE = 0.5f; + private static final int AMBIENT_SHADOW_ALPHA = 7; + + private final int mIconSize; + + private final Paint mBlurPaint; + private final Paint mDrawPaint; + private final BlurMaskFilter mDefaultBlurMaskFilter; + + public ShadowGenerator(int iconSize) { + mIconSize = iconSize; + mBlurPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); + mDrawPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); + mDefaultBlurMaskFilter = new BlurMaskFilter(mIconSize * BLUR_FACTOR, Blur.NORMAL); + } + + public synchronized void recreateIcon(Bitmap icon, Canvas out) { + recreateIcon(icon, mDefaultBlurMaskFilter, AMBIENT_SHADOW_ALPHA, KEY_SHADOW_ALPHA, out); + } + + public synchronized void recreateIcon(Bitmap icon, BlurMaskFilter blurMaskFilter, + int ambientAlpha, int keyAlpha, Canvas out) { + if (ENABLE_SHADOWS) { + int[] offset = new int[2]; + mBlurPaint.setMaskFilter(blurMaskFilter); + Bitmap shadow = icon.extractAlpha(mBlurPaint, offset); + + // Draw ambient shadow + mDrawPaint.setAlpha(ambientAlpha); + out.drawBitmap(shadow, offset[0], offset[1], mDrawPaint); + + // Draw key shadow + mDrawPaint.setAlpha(keyAlpha); + out.drawBitmap(shadow, offset[0], offset[1] + KEY_SHADOW_DISTANCE * mIconSize, + mDrawPaint); + } + + // Draw the icon + mDrawPaint.setAlpha(255); + out.drawBitmap(icon, 0, 0, mDrawPaint); + } + + /** + * Returns the minimum amount by which an icon with {@param bounds} should be scaled + * so that the shadows do not get clipped. + */ + public static float getScaleForBounds(RectF bounds) { + float scale = 1; + + if (ENABLE_SHADOWS) { + // For top, left & right, we need same space. + float minSide = Math.min(Math.min(bounds.left, bounds.right), bounds.top); + if (minSide < BLUR_FACTOR) { + scale = (HALF_DISTANCE - BLUR_FACTOR) / (HALF_DISTANCE - minSide); + } + + float bottomSpace = BLUR_FACTOR + KEY_SHADOW_DISTANCE; + if (bounds.bottom < bottomSpace) { + scale = Math.min(scale, + (HALF_DISTANCE - bottomSpace) / (HALF_DISTANCE - bounds.bottom)); + } + } + return scale; + } + + public static class Builder { + + public final RectF bounds = new RectF(); + public final int color; + + public int ambientShadowAlpha = AMBIENT_SHADOW_ALPHA; + + public float shadowBlur; + + public float keyShadowDistance; + public int keyShadowAlpha = KEY_SHADOW_ALPHA; + public float radius; + + public Builder(int color) { + this.color = color; + } + + public Builder setupBlurForSize(int height) { + if (ENABLE_SHADOWS) { + shadowBlur = height * 1f / 24; + keyShadowDistance = height * 1f / 16; + } else { + shadowBlur = 0; + keyShadowDistance = 0; + } + return this; + } + + public Bitmap createPill(int width, int height) { + return createPill(width, height, height / 2f); + } + + public Bitmap createPill(int width, int height, float r) { + radius = r; + + int centerX = Math.round(width / 2f + shadowBlur); + int centerY = Math.round(radius + shadowBlur + keyShadowDistance); + int center = Math.max(centerX, centerY); + bounds.set(0, 0, width, height); + bounds.offsetTo(center - width / 2f, center - height / 2f); + + int size = center * 2; + return BitmapRenderer.createHardwareBitmap(size, size, this::drawShadow); + } + + public void drawShadow(Canvas c) { + Paint p = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); + p.setColor(color); + + if (ENABLE_SHADOWS) { + // Key shadow + p.setShadowLayer(shadowBlur, 0, keyShadowDistance, + setColorAlphaBound(Color.BLACK, keyShadowAlpha)); + c.drawRoundRect(bounds, radius, radius, p); + + // Ambient shadow + p.setShadowLayer(shadowBlur, 0, 0, + setColorAlphaBound(Color.BLACK, ambientShadowAlpha)); + c.drawRoundRect(bounds, radius, radius, p); + } + + if (Color.alpha(color) < 255) { + // Clear any content inside the pill-rect for translucent fill. + p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + p.clearShadowLayer(); + p.setColor(Color.BLACK); + c.drawRoundRect(bounds, radius, radius, p); + + p.setXfermode(null); + p.setColor(color); + c.drawRoundRect(bounds, radius, radius, p); + } + } + } +} diff --git a/PermissionController/iconloaderlib/src/com/android/launcher3/icons/ThemedIconDrawable.java b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/ThemedIconDrawable.java new file mode 100644 index 000000000..b2e554b7b --- /dev/null +++ b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/ThemedIconDrawable.java @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.icons; + +import static android.content.res.Configuration.UI_MODE_NIGHT_MASK; +import static android.content.res.Configuration.UI_MODE_NIGHT_YES; +import static android.content.res.Resources.ID_NULL; + +import static com.android.launcher3.icons.GraphicsUtils.getExpectedBitmapSize; +import static com.android.launcher3.icons.IconProvider.ICON_TYPE_CALENDAR; +import static com.android.launcher3.icons.IconProvider.ICON_TYPE_CLOCK; + +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.Rect; +import android.graphics.drawable.AdaptiveIconDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; +import android.os.Process; +import android.os.UserHandle; +import android.util.Log; + +import androidx.annotation.Nullable; + +import com.android.launcher3.icons.BitmapInfo.Extender; +import com.android.launcher3.icons.cache.BaseIconCache; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** + * Class to handle monochrome themed app icons + */ +@SuppressWarnings("NewApi") +public class ThemedIconDrawable extends FastBitmapDrawable { + + public static final String TAG = "ThemedIconDrawable"; + + final ThemedBitmapInfo bitmapInfo; + final int colorFg, colorBg; + + // The foreground/monochrome icon for the app + private final Drawable mMonochromeIcon; + private final AdaptiveIconDrawable mBgWrapper; + private final Rect mBadgeBounds; + + protected ThemedIconDrawable(ThemedConstantState constantState) { + super(constantState.mBitmap, constantState.colorFg, constantState.mIsDisabled); + bitmapInfo = constantState.bitmapInfo; + colorBg = constantState.colorBg; + colorFg = constantState.colorFg; + + mMonochromeIcon = bitmapInfo.mThemeData.loadMonochromeDrawable(colorFg); + mBgWrapper = new AdaptiveIconDrawable(new ColorDrawable(colorBg), null); + mBadgeBounds = bitmapInfo.mUserBadge == null ? null : + new Rect(0, 0, bitmapInfo.mUserBadge.getWidth(), bitmapInfo.mUserBadge.getHeight()); + + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + mBgWrapper.setBounds(bounds); + mMonochromeIcon.setBounds(bounds); + } + + @Override + protected void drawInternal(Canvas canvas, Rect bounds) { + int count = canvas.save(); + canvas.scale(bitmapInfo.mNormalizationScale, bitmapInfo.mNormalizationScale, + bounds.exactCenterX(), bounds.exactCenterY()); + mPaint.setColor(colorBg); + canvas.drawPath(mBgWrapper.getIconMask(), mPaint); + mMonochromeIcon.draw(canvas); + canvas.restoreToCount(count); + if (mBadgeBounds != null) { + canvas.drawBitmap(bitmapInfo.mUserBadge, mBadgeBounds, getBounds(), mPaint); + } + } + + @Override + public boolean isThemed() { + return true; + } + + @Override + public ConstantState getConstantState() { + return new ThemedConstantState(bitmapInfo, colorBg, colorFg, mIsDisabled); + } + + static class ThemedConstantState extends FastBitmapConstantState { + + final ThemedBitmapInfo bitmapInfo; + final int colorFg, colorBg; + + public ThemedConstantState(ThemedBitmapInfo bitmapInfo, + int colorBg, int colorFg, boolean isDisabled) { + super(bitmapInfo.icon, bitmapInfo.color, isDisabled); + this.bitmapInfo = bitmapInfo; + this.colorBg = colorBg; + this.colorFg = colorFg; + } + + @Override + public FastBitmapDrawable newDrawable() { + return new ThemedIconDrawable(this); + } + } + + public static class ThemedBitmapInfo extends BitmapInfo { + + final ThemeData mThemeData; + final float mNormalizationScale; + final Bitmap mUserBadge; + + public ThemedBitmapInfo(Bitmap icon, int color, ThemeData themeData, + float normalizationScale, Bitmap userBadge) { + super(icon, color); + mThemeData = themeData; + mNormalizationScale = normalizationScale; + mUserBadge = userBadge; + } + + @Override + public FastBitmapDrawable newThemedIcon(Context context) { + int[] colors = getColors(context); + FastBitmapDrawable drawable = new ThemedConstantState(this, colors[0], colors[1], false) + .newDrawable(); + drawable.mDisabledAlpha = GraphicsUtils.getFloat(context, R.attr.disabledIconAlpha, 1f); + return drawable; + } + + @Nullable + public byte[] toByteArray() { + if (isNullOrLowRes()) { + return null; + } + String resName = mThemeData.mResources.getResourceName(mThemeData.mResID); + ByteArrayOutputStream out = new ByteArrayOutputStream( + getExpectedBitmapSize(icon) + 3 + resName.length()); + try { + DataOutputStream dos = new DataOutputStream(out); + dos.writeByte(TYPE_THEMED); + dos.writeFloat(mNormalizationScale); + dos.writeUTF(resName); + icon.compress(Bitmap.CompressFormat.PNG, 100, dos); + + dos.flush(); + dos.close(); + return out.toByteArray(); + } catch (IOException e) { + Log.w(TAG, "Could not write bitmap"); + return null; + } + } + + static ThemedBitmapInfo decode(byte[] data, int color, + BitmapFactory.Options decodeOptions, UserHandle user, BaseIconCache iconCache, + Context context) { + try (DataInputStream dis = new DataInputStream(new ByteArrayInputStream(data))) { + dis.readByte(); // type + float normalizationScale = dis.readFloat(); + + String resName = dis.readUTF(); + int resId = context.getResources() + .getIdentifier(resName, "drawable", context.getPackageName()); + if (resId == ID_NULL) { + return null; + } + + Bitmap userBadgeBitmap = null; + if (!Process.myUserHandle().equals(user)) { + try (BaseIconFactory iconFactory = iconCache.getIconFactory()) { + userBadgeBitmap = iconFactory.getUserBadgeBitmap(user); + } + } + + ThemeData themeData = new ThemeData(context.getResources(), resId); + Bitmap icon = BitmapFactory.decodeStream(dis, null, decodeOptions); + return new ThemedBitmapInfo(icon, color, themeData, normalizationScale, + userBadgeBitmap); + } catch (IOException e) { + return null; + } + } + } + + public static class ThemeData { + + final Resources mResources; + final int mResID; + + public ThemeData(Resources resources, int resID) { + mResources = resources; + mResID = resID; + } + + Drawable loadMonochromeDrawable(int accentColor) { + Drawable d = mResources.getDrawable(mResID).mutate(); + d.setTint(accentColor); + d = new InsetDrawable(d, .2f); + return d; + } + + public Drawable wrapDrawable(Drawable original, int iconType) { + if (!(original instanceof AdaptiveIconDrawable)) { + return original; + } + AdaptiveIconDrawable aid = (AdaptiveIconDrawable) original; + String resourceType = mResources.getResourceTypeName(mResID); + if (iconType == ICON_TYPE_CALENDAR && "array".equals(resourceType)) { + TypedArray ta = mResources.obtainTypedArray(mResID); + int id = ta.getResourceId(IconProvider.getDay(), ID_NULL); + ta.recycle(); + return id == ID_NULL ? original + : new ThemedAdaptiveIcon(aid, new ThemeData(mResources, id)); + } else if (iconType == ICON_TYPE_CLOCK && "array".equals(resourceType)) { + ((ClockDrawableWrapper) original).mThemeData = this; + return original; + } else if ("drawable".equals(resourceType)) { + return new ThemedAdaptiveIcon(aid, this); + } else { + return original; + } + } + } + + static class ThemedAdaptiveIcon extends AdaptiveIconDrawable implements Extender { + + protected final ThemeData mThemeData; + + public ThemedAdaptiveIcon(AdaptiveIconDrawable parent, ThemeData themeData) { + super(parent.getBackground(), parent.getForeground()); + mThemeData = themeData; + } + + @Override + public BitmapInfo getExtendedInfo(Bitmap bitmap, int color, BaseIconFactory iconFactory, + float normalizationScale, UserHandle user) { + Bitmap userBadge = Process.myUserHandle().equals(user) + ? null : iconFactory.getUserBadgeBitmap(user); + return new ThemedBitmapInfo(bitmap, color, mThemeData, normalizationScale, userBadge); + } + + @Override + public void drawForPersistence(Canvas canvas) { + draw(canvas); + } + + @Override + public Drawable getThemedDrawable(Context context) { + int[] colors = getColors(context); + Drawable bg = new ColorDrawable(colors[0]); + float inset = getExtraInsetFraction() / (1 + 2 * getExtraInsetFraction()); + Drawable fg = new InsetDrawable(mThemeData.loadMonochromeDrawable(colors[1]), inset); + return new AdaptiveIconDrawable(bg, fg); + } + } + + /** + * Get an int array representing background and foreground colors for themed icons + */ + public static int[] getColors(Context context) { + Resources res = context.getResources(); + int[] colors = new int[2]; + if ((res.getConfiguration().uiMode & UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES) { + colors[0] = res.getColor(android.R.color.system_neutral1_800); + colors[1] = res.getColor(android.R.color.system_accent1_100); + } else { + colors[0] = res.getColor(android.R.color.system_accent1_100); + colors[1] = res.getColor(android.R.color.system_neutral2_700); + } + return colors; + } +} diff --git a/PermissionController/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java new file mode 100644 index 000000000..d685737c4 --- /dev/null +++ b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java @@ -0,0 +1,570 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.icons.cache; + +import static com.android.launcher3.icons.BaseIconFactory.getFullResDefaultActivityIcon; +import static com.android.launcher3.icons.BitmapInfo.LOW_RES_ICON; +import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound; + +import android.content.ComponentName; +import android.content.ContentValues; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.Resources; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Handler; +import android.os.LocaleList; +import android.os.Looper; +import android.os.Process; +import android.os.UserHandle; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.launcher3.icons.BaseIconFactory; +import com.android.launcher3.icons.BitmapInfo; +import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.SQLiteCacheHelper; + +import java.util.AbstractMap; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +public abstract class BaseIconCache { + + private static final String TAG = "BaseIconCache"; + private static final boolean DEBUG = false; + + private static final int INITIAL_ICON_CACHE_CAPACITY = 50; + + // Empty class name is used for storing package default entry. + public static final String EMPTY_CLASS_NAME = "."; + + public static class CacheEntry { + + @NonNull + public BitmapInfo bitmap = BitmapInfo.LOW_RES_INFO; + public CharSequence title = ""; + public CharSequence contentDescription = ""; + } + + private final HashMap<UserHandle, BitmapInfo> mDefaultIcons = new HashMap<>(); + + protected final Context mContext; + protected final PackageManager mPackageManager; + + private final Map<ComponentKey, CacheEntry> mCache; + protected final Handler mWorkerHandler; + + protected int mIconDpi; + protected IconDB mIconDb; + protected LocaleList mLocaleList = LocaleList.getEmptyLocaleList(); + protected String mSystemState = ""; + + private final String mDbFileName; + private final Looper mBgLooper; + + public BaseIconCache(Context context, String dbFileName, Looper bgLooper, + int iconDpi, int iconPixelSize, boolean inMemoryCache) { + mContext = context; + mDbFileName = dbFileName; + mPackageManager = context.getPackageManager(); + mBgLooper = bgLooper; + mWorkerHandler = new Handler(mBgLooper); + + if (inMemoryCache) { + mCache = new HashMap<>(INITIAL_ICON_CACHE_CAPACITY); + } else { + // Use a dummy cache + mCache = new AbstractMap<ComponentKey, CacheEntry>() { + @Override + public Set<Entry<ComponentKey, CacheEntry>> entrySet() { + return Collections.emptySet(); + } + + @Override + public CacheEntry put(ComponentKey key, CacheEntry value) { + return value; + } + }; + } + + updateSystemState(); + mIconDpi = iconDpi; + mIconDb = new IconDB(context, dbFileName, iconPixelSize); + } + + /** + * Returns the persistable serial number for {@param user}. Subclass should implement proper + * caching strategy to avoid making binder call every time. + */ + protected abstract long getSerialNumberForUser(UserHandle user); + + /** + * Return true if the given app is an instant app and should be badged appropriately. + */ + protected abstract boolean isInstantApp(ApplicationInfo info); + + /** + * Opens and returns an icon factory. The factory is recycled by the caller. + */ + public abstract BaseIconFactory getIconFactory(); + + public void updateIconParams(int iconDpi, int iconPixelSize) { + mWorkerHandler.post(() -> updateIconParamsBg(iconDpi, iconPixelSize)); + } + + private synchronized void updateIconParamsBg(int iconDpi, int iconPixelSize) { + mIconDpi = iconDpi; + mDefaultIcons.clear(); + mIconDb.clear(); + mIconDb.close(); + mIconDb = new IconDB(mContext, mDbFileName, iconPixelSize); + mCache.clear(); + } + + private Drawable getFullResIcon(Resources resources, int iconId) { + if (resources != null && iconId != 0) { + try { + return resources.getDrawableForDensity(iconId, mIconDpi); + } catch (Resources.NotFoundException e) { } + } + return getFullResDefaultActivityIcon(mIconDpi); + } + + public Drawable getFullResIcon(String packageName, int iconId) { + try { + return getFullResIcon(mPackageManager.getResourcesForApplication(packageName), iconId); + } catch (PackageManager.NameNotFoundException e) { } + return getFullResDefaultActivityIcon(mIconDpi); + } + + public Drawable getFullResIcon(ActivityInfo info) { + try { + return getFullResIcon(mPackageManager.getResourcesForApplication(info.applicationInfo), + info.getIconResource()); + } catch (PackageManager.NameNotFoundException e) { } + return getFullResDefaultActivityIcon(mIconDpi); + } + + private BitmapInfo makeDefaultIcon(UserHandle user) { + try (BaseIconFactory li = getIconFactory()) { + return li.makeDefaultIcon(user); + } + } + + /** + * Remove any records for the supplied ComponentName. + */ + public synchronized void remove(ComponentName componentName, UserHandle user) { + mCache.remove(new ComponentKey(componentName, user)); + } + + /** + * Remove any records for the supplied package name from memory. + */ + private void removeFromMemCacheLocked(String packageName, UserHandle user) { + HashSet<ComponentKey> forDeletion = new HashSet<>(); + for (ComponentKey key: mCache.keySet()) { + if (key.componentName.getPackageName().equals(packageName) + && key.user.equals(user)) { + forDeletion.add(key); + } + } + for (ComponentKey condemned: forDeletion) { + mCache.remove(condemned); + } + } + + /** + * Removes the entries related to the given package in memory and persistent DB. + */ + public synchronized void removeIconsForPkg(String packageName, UserHandle user) { + removeFromMemCacheLocked(packageName, user); + long userSerial = getSerialNumberForUser(user); + mIconDb.delete( + IconDB.COLUMN_COMPONENT + " LIKE ? AND " + IconDB.COLUMN_USER + " = ?", + new String[]{packageName + "/%", Long.toString(userSerial)}); + } + + public IconCacheUpdateHandler getUpdateHandler() { + updateSystemState(); + return new IconCacheUpdateHandler(this); + } + + /** + * Refreshes the system state definition used to check the validity of the cache. It + * incorporates all the properties that can affect the cache like the list of enabled locale + * and system-version. + */ + private void updateSystemState() { + mLocaleList = mContext.getResources().getConfiguration().getLocales(); + mSystemState = mLocaleList.toLanguageTags() + "," + Build.VERSION.SDK_INT; + } + + protected String getIconSystemState(String packageName) { + return mSystemState; + } + + /** + * Adds an entry into the DB and the in-memory cache. + * @param replaceExisting if true, it will recreate the bitmap even if it already exists in + * the memory. This is useful then the previous bitmap was created using + * old data. + */ + @VisibleForTesting + public synchronized <T> void addIconToDBAndMemCache(T object, CachingLogic<T> cachingLogic, + PackageInfo info, long userSerial, boolean replaceExisting) { + UserHandle user = cachingLogic.getUser(object); + ComponentName componentName = cachingLogic.getComponent(object); + + final ComponentKey key = new ComponentKey(componentName, user); + CacheEntry entry = null; + if (!replaceExisting) { + entry = mCache.get(key); + // We can't reuse the entry if the high-res icon is not present. + if (entry == null || entry.bitmap.isNullOrLowRes()) { + entry = null; + } + } + if (entry == null) { + entry = new CacheEntry(); + entry.bitmap = cachingLogic.loadIcon(mContext, object); + } + // Icon can't be loaded from cachingLogic, which implies alternative icon was loaded + // (e.g. fallback icon, default icon). So we drop here since there's no point in caching + // an empty entry. + if (entry.bitmap.isNullOrLowRes()) return; + entry.title = cachingLogic.getLabel(object); + entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user); + if (cachingLogic.addToMemCache()) mCache.put(key, entry); + + ContentValues values = newContentValues(entry.bitmap, entry.title.toString(), + componentName.getPackageName(), cachingLogic.getKeywords(object, mLocaleList)); + addIconToDB(values, componentName, info, userSerial, + cachingLogic.getLastUpdatedTime(object, info)); + } + + /** + * Updates {@param values} to contain versioning information and adds it to the DB. + * @param values {@link ContentValues} containing icon & title + */ + private void addIconToDB(ContentValues values, ComponentName key, + PackageInfo info, long userSerial, long lastUpdateTime) { + values.put(IconDB.COLUMN_COMPONENT, key.flattenToString()); + values.put(IconDB.COLUMN_USER, userSerial); + values.put(IconDB.COLUMN_LAST_UPDATED, lastUpdateTime); + values.put(IconDB.COLUMN_VERSION, info.versionCode); + mIconDb.insertOrReplace(values); + } + + public synchronized BitmapInfo getDefaultIcon(UserHandle user) { + if (!mDefaultIcons.containsKey(user)) { + mDefaultIcons.put(user, makeDefaultIcon(user)); + } + return mDefaultIcons.get(user); + } + + public boolean isDefaultIcon(BitmapInfo icon, UserHandle user) { + return getDefaultIcon(user).icon == icon.icon; + } + + /** + * Retrieves the entry from the cache. If the entry is not present, it creates a new entry. + * This method is not thread safe, it must be called from a synchronized method. + */ + protected <T> CacheEntry cacheLocked( + @NonNull ComponentName componentName, @NonNull UserHandle user, + @NonNull Supplier<T> infoProvider, @NonNull CachingLogic<T> cachingLogic, + boolean usePackageIcon, boolean useLowResIcon) { + assertWorkerThread(); + ComponentKey cacheKey = new ComponentKey(componentName, user); + CacheEntry entry = mCache.get(cacheKey); + if (entry == null || (entry.bitmap.isLowRes() && !useLowResIcon)) { + entry = new CacheEntry(); + if (cachingLogic.addToMemCache()) { + mCache.put(cacheKey, entry); + } + + // Check the DB first. + T object = null; + boolean providerFetchedOnce = false; + + if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) { + object = infoProvider.get(); + providerFetchedOnce = true; + + if (object != null) { + entry.bitmap = cachingLogic.loadIcon(mContext, object); + } else { + if (usePackageIcon) { + CacheEntry packageEntry = getEntryForPackageLocked( + componentName.getPackageName(), user, false); + if (packageEntry != null) { + if (DEBUG) Log.d(TAG, "using package default icon for " + + componentName.toShortString()); + entry.bitmap = packageEntry.bitmap; + entry.title = packageEntry.title; + entry.contentDescription = packageEntry.contentDescription; + } + } + if (entry.bitmap == null) { + if (DEBUG) Log.d(TAG, "using default icon for " + + componentName.toShortString()); + entry.bitmap = getDefaultIcon(user); + } + } + } + + if (TextUtils.isEmpty(entry.title)) { + if (object == null && !providerFetchedOnce) { + object = infoProvider.get(); + providerFetchedOnce = true; + } + if (object != null) { + entry.title = cachingLogic.getLabel(object); + entry.contentDescription = mPackageManager.getUserBadgedLabel( + cachingLogic.getDescription(object, entry.title), user); + } + } + } + return entry; + } + + public synchronized void clear() { + assertWorkerThread(); + mIconDb.clear(); + } + + /** + * Adds a default package entry in the cache. This entry is not persisted and will be removed + * when the cache is flushed. + */ + protected synchronized void cachePackageInstallInfo(String packageName, UserHandle user, + Bitmap icon, CharSequence title) { + removeFromMemCacheLocked(packageName, user); + + ComponentKey cacheKey = getPackageKey(packageName, user); + CacheEntry entry = mCache.get(cacheKey); + + // For icon caching, do not go through DB. Just update the in-memory entry. + if (entry == null) { + entry = new CacheEntry(); + } + if (!TextUtils.isEmpty(title)) { + entry.title = title; + } + if (icon != null) { + BaseIconFactory li = getIconFactory(); + entry.bitmap = li.createShapedIconBitmap(icon, user); + li.close(); + } + if (!TextUtils.isEmpty(title) && entry.bitmap.icon != null) { + mCache.put(cacheKey, entry); + } + } + + private static ComponentKey getPackageKey(String packageName, UserHandle user) { + ComponentName cn = new ComponentName(packageName, packageName + EMPTY_CLASS_NAME); + return new ComponentKey(cn, user); + } + + /** + * Gets an entry for the package, which can be used as a fallback entry for various components. + * This method is not thread safe, it must be called from a synchronized method. + */ + protected CacheEntry getEntryForPackageLocked(String packageName, UserHandle user, + boolean useLowResIcon) { + assertWorkerThread(); + ComponentKey cacheKey = getPackageKey(packageName, user); + CacheEntry entry = mCache.get(cacheKey); + + if (entry == null || (entry.bitmap.isLowRes() && !useLowResIcon)) { + entry = new CacheEntry(); + boolean entryUpdated = true; + + // Check the DB first. + if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) { + try { + int flags = Process.myUserHandle().equals(user) ? 0 : + PackageManager.GET_UNINSTALLED_PACKAGES; + PackageInfo info = mPackageManager.getPackageInfo(packageName, flags); + ApplicationInfo appInfo = info.applicationInfo; + if (appInfo == null) { + throw new NameNotFoundException("ApplicationInfo is null"); + } + + BaseIconFactory li = getIconFactory(); + // Load the full res icon for the application, but if useLowResIcon is set, then + // only keep the low resolution icon instead of the larger full-sized icon + BitmapInfo iconInfo = li.createBadgedIconBitmap( + appInfo.loadIcon(mPackageManager), user, appInfo.targetSdkVersion, + isInstantApp(appInfo)); + li.close(); + + entry.title = appInfo.loadLabel(mPackageManager); + entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user); + entry.bitmap = BitmapInfo.of( + useLowResIcon ? LOW_RES_ICON : iconInfo.icon, iconInfo.color); + + // Add the icon in the DB here, since these do not get written during + // package updates. + ContentValues values = newContentValues( + iconInfo, entry.title.toString(), packageName, null); + addIconToDB(values, cacheKey.componentName, info, getSerialNumberForUser(user), + info.lastUpdateTime); + + } catch (NameNotFoundException e) { + if (DEBUG) Log.d(TAG, "Application not installed " + packageName); + entryUpdated = false; + } + } + + // Only add a filled-out entry to the cache + if (entryUpdated) { + mCache.put(cacheKey, entry); + } + } + return entry; + } + + protected boolean getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes) { + Cursor c = null; + try { + c = mIconDb.query( + lowRes ? IconDB.COLUMNS_LOW_RES : IconDB.COLUMNS_HIGH_RES, + IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?", + new String[]{ + cacheKey.componentName.flattenToString(), + Long.toString(getSerialNumberForUser(cacheKey.user))}); + if (c.moveToNext()) { + // Set the alpha to be 255, so that we never have a wrong color + entry.bitmap = BitmapInfo.of(LOW_RES_ICON, setColorAlphaBound(c.getInt(0), 255)); + entry.title = c.getString(1); + if (entry.title == null) { + entry.title = ""; + entry.contentDescription = ""; + } else { + entry.contentDescription = mPackageManager.getUserBadgedLabel( + entry.title, cacheKey.user); + } + + if (!lowRes) { + try { + entry.bitmap = BitmapInfo.fromByteArray( + c.getBlob(2), entry.bitmap.color, cacheKey.user, this, mContext); + } catch (Exception e) { + return false; + } + } + return entry.bitmap != null; + } + } catch (SQLiteException e) { + Log.d(TAG, "Error reading icon cache", e); + } finally { + if (c != null) { + c.close(); + } + } + return false; + } + + /** + * Returns a cursor for an arbitrary query to the cache db + */ + public synchronized Cursor queryCacheDb(String[] columns, String selection, + String[] selectionArgs) { + return mIconDb.query(columns, selection, selectionArgs); + } + + /** + * Cache class to store the actual entries on disk + */ + public static final class IconDB extends SQLiteCacheHelper { + private static final int RELEASE_VERSION = 31; + + public static final String TABLE_NAME = "icons"; + public static final String COLUMN_ROWID = "rowid"; + public static final String COLUMN_COMPONENT = "componentName"; + public static final String COLUMN_USER = "profileId"; + public static final String COLUMN_LAST_UPDATED = "lastUpdated"; + public static final String COLUMN_VERSION = "version"; + public static final String COLUMN_ICON = "icon"; + public static final String COLUMN_ICON_COLOR = "icon_color"; + public static final String COLUMN_LABEL = "label"; + public static final String COLUMN_SYSTEM_STATE = "system_state"; + public static final String COLUMN_KEYWORDS = "keywords"; + + public static final String[] COLUMNS_HIGH_RES = new String[] { + IconDB.COLUMN_ICON_COLOR, IconDB.COLUMN_LABEL, IconDB.COLUMN_ICON }; + public static final String[] COLUMNS_LOW_RES = new String[] { + IconDB.COLUMN_ICON_COLOR, IconDB.COLUMN_LABEL }; + + public IconDB(Context context, String dbFileName, int iconPixelSize) { + super(context, dbFileName, (RELEASE_VERSION << 16) + iconPixelSize, TABLE_NAME); + } + + @Override + protected void onCreateTable(SQLiteDatabase db) { + db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" + + COLUMN_COMPONENT + " TEXT NOT NULL, " + + COLUMN_USER + " INTEGER NOT NULL, " + + COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " + + COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " + + COLUMN_ICON + " BLOB, " + + COLUMN_ICON_COLOR + " INTEGER NOT NULL DEFAULT 0, " + + COLUMN_LABEL + " TEXT, " + + COLUMN_SYSTEM_STATE + " TEXT, " + + COLUMN_KEYWORDS + " TEXT, " + + "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") " + + ");"); + } + } + + private ContentValues newContentValues(BitmapInfo bitmapInfo, String label, + String packageName, @Nullable String keywords) { + ContentValues values = new ContentValues(); + values.put(IconDB.COLUMN_ICON, bitmapInfo.toByteArray()); + values.put(IconDB.COLUMN_ICON_COLOR, bitmapInfo.color); + + values.put(IconDB.COLUMN_LABEL, label); + values.put(IconDB.COLUMN_SYSTEM_STATE, getIconSystemState(packageName)); + values.put(IconDB.COLUMN_KEYWORDS, keywords); + return values; + } + + private void assertWorkerThread() { + if (Looper.myLooper() != mBgLooper) { + throw new IllegalStateException("Cache accessed on wrong thread " + Looper.myLooper()); + } + } +} diff --git a/PermissionController/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java new file mode 100644 index 000000000..c12e9dcc1 --- /dev/null +++ b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.icons.cache; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.os.LocaleList; +import android.os.UserHandle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.launcher3.icons.BitmapInfo; + +public interface CachingLogic<T> { + + ComponentName getComponent(T object); + + UserHandle getUser(T object); + + CharSequence getLabel(T object); + + default CharSequence getDescription(T object, CharSequence fallback) { + return fallback; + } + + @NonNull + BitmapInfo loadIcon(Context context, T object); + + /** + * Provides a option list of keywords to associate with this object + */ + @Nullable + default String getKeywords(T object, LocaleList localeList) { + return null; + } + + /** + * Returns the timestamp the entry was last updated in cache. + */ + default long getLastUpdatedTime(T object, PackageInfo info) { + return info.lastUpdateTime; + } + + /** + * Returns true the object should be added to mem cache; otherwise returns false. + */ + default boolean addToMemCache() { + return true; + } +} diff --git a/PermissionController/iconloaderlib/src/com/android/launcher3/icons/cache/HandlerRunnable.java b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/cache/HandlerRunnable.java new file mode 100644 index 000000000..3dfb3840f --- /dev/null +++ b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/cache/HandlerRunnable.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.icons.cache; + +import android.os.Handler; + +import java.util.concurrent.Executor; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * A runnable that can be posted to a {@link Handler} which can be canceled. + */ +public class HandlerRunnable<T> implements Runnable { + + private final Handler mWorkerHandler; + private final Supplier<T> mTask; + + private final Executor mCallbackExecutor; + private final Consumer<T> mCallback; + private final Runnable mEndRunnable; + + private boolean mEnded = false; + private boolean mCanceled = false; + + public HandlerRunnable(Handler workerHandler, Supplier<T> task, Executor callbackExecutor, + Consumer<T> callback) { + this(workerHandler, task, callbackExecutor, callback, () -> { }); + } + + public HandlerRunnable(Handler workerHandler, Supplier<T> task, Executor callbackExecutor, + Consumer<T> callback, Runnable endRunnable) { + mWorkerHandler = workerHandler; + mTask = task; + mCallbackExecutor = callbackExecutor; + mCallback = callback; + mEndRunnable = endRunnable; + } + + /** + * Cancels this runnable from being run, only if it has not already run. + */ + public void cancel() { + mWorkerHandler.removeCallbacks(this); + mCanceled = true; + mCallbackExecutor.execute(this::onEnd); + } + + @Override + public void run() { + T value = mTask.get(); + mCallbackExecutor.execute(() -> { + if (!mCanceled) { + mCallback.accept(value); + } + onEnd(); + }); + } + + private void onEnd() { + if (!mEnded) { + mEnded = true; + mEndRunnable.run(); + } + } +} diff --git a/PermissionController/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java new file mode 100644 index 000000000..9e1ad7b7b --- /dev/null +++ b/PermissionController/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.icons.cache; + +import android.content.ComponentName; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.database.sqlite.SQLiteException; +import android.os.SystemClock; +import android.os.UserHandle; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Log; +import android.util.SparseBooleanArray; + +import com.android.launcher3.icons.cache.BaseIconCache.IconDB; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; +import java.util.Stack; + +/** + * Utility class to handle updating the Icon cache + */ +public class IconCacheUpdateHandler { + + private static final String TAG = "IconCacheUpdateHandler"; + + /** + * In this mode, all invalid icons are marked as to-be-deleted in {@link #mItemsToDelete}. + * This mode is used for the first run. + */ + private static final boolean MODE_SET_INVALID_ITEMS = true; + + /** + * In this mode, any valid icon is removed from {@link #mItemsToDelete}. This is used for all + * subsequent runs, which essentially acts as set-union of all valid items. + */ + private static final boolean MODE_CLEAR_VALID_ITEMS = false; + + private static final Object ICON_UPDATE_TOKEN = new Object(); + + private final HashMap<String, PackageInfo> mPkgInfoMap; + private final BaseIconCache mIconCache; + + private final ArrayMap<UserHandle, Set<String>> mPackagesToIgnore = new ArrayMap<>(); + + private final SparseBooleanArray mItemsToDelete = new SparseBooleanArray(); + private boolean mFilterMode = MODE_SET_INVALID_ITEMS; + + IconCacheUpdateHandler(BaseIconCache cache) { + mIconCache = cache; + + mPkgInfoMap = new HashMap<>(); + + // Remove all active icon update tasks. + mIconCache.mWorkerHandler.removeCallbacksAndMessages(ICON_UPDATE_TOKEN); + + createPackageInfoMap(); + } + + /** + * Sets a package to ignore for processing + */ + public void addPackagesToIgnore(UserHandle userHandle, String packageName) { + Set<String> packages = mPackagesToIgnore.get(userHandle); + if (packages == null) { + packages = new HashSet<>(); + mPackagesToIgnore.put(userHandle, packages); + } + packages.add(packageName); + } + + private void createPackageInfoMap() { + PackageManager pm = mIconCache.mPackageManager; + for (PackageInfo info : + pm.getInstalledPackages(PackageManager.MATCH_UNINSTALLED_PACKAGES)) { + mPkgInfoMap.put(info.packageName, info); + } + } + + /** + * Updates the persistent DB, such that only entries corresponding to {@param apps} remain in + * the DB and are updated. + * @return The set of packages for which icons have updated. + */ + public <T> void updateIcons(List<T> apps, CachingLogic<T> cachingLogic, + OnUpdateCallback onUpdateCallback) { + // Filter the list per user + HashMap<UserHandle, HashMap<ComponentName, T>> userComponentMap = new HashMap<>(); + int count = apps.size(); + for (int i = 0; i < count; i++) { + T app = apps.get(i); + UserHandle userHandle = cachingLogic.getUser(app); + HashMap<ComponentName, T> componentMap = userComponentMap.get(userHandle); + if (componentMap == null) { + componentMap = new HashMap<>(); + userComponentMap.put(userHandle, componentMap); + } + componentMap.put(cachingLogic.getComponent(app), app); + } + + for (Entry<UserHandle, HashMap<ComponentName, T>> entry : userComponentMap.entrySet()) { + updateIconsPerUser(entry.getKey(), entry.getValue(), cachingLogic, onUpdateCallback); + } + + // From now on, clear every valid item from the global valid map. + mFilterMode = MODE_CLEAR_VALID_ITEMS; + } + + /** + * Updates the persistent DB, such that only entries corresponding to {@param apps} remain in + * the DB and are updated. + * @return The set of packages for which icons have updated. + */ + @SuppressWarnings("unchecked") + private <T> void updateIconsPerUser(UserHandle user, HashMap<ComponentName, T> componentMap, + CachingLogic<T> cachingLogic, OnUpdateCallback onUpdateCallback) { + Set<String> ignorePackages = mPackagesToIgnore.get(user); + if (ignorePackages == null) { + ignorePackages = Collections.emptySet(); + } + long userSerial = mIconCache.getSerialNumberForUser(user); + + Stack<T> appsToUpdate = new Stack<>(); + + try (Cursor c = mIconCache.mIconDb.query( + new String[]{IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT, + IconDB.COLUMN_LAST_UPDATED, IconDB.COLUMN_VERSION, + IconDB.COLUMN_SYSTEM_STATE}, + IconDB.COLUMN_USER + " = ? ", + new String[]{Long.toString(userSerial)})) { + + final int indexComponent = c.getColumnIndex(IconDB.COLUMN_COMPONENT); + final int indexLastUpdate = c.getColumnIndex(IconDB.COLUMN_LAST_UPDATED); + final int indexVersion = c.getColumnIndex(IconDB.COLUMN_VERSION); + final int rowIndex = c.getColumnIndex(IconDB.COLUMN_ROWID); + final int systemStateIndex = c.getColumnIndex(IconDB.COLUMN_SYSTEM_STATE); + + while (c.moveToNext()) { + String cn = c.getString(indexComponent); + ComponentName component = ComponentName.unflattenFromString(cn); + PackageInfo info = mPkgInfoMap.get(component.getPackageName()); + + int rowId = c.getInt(rowIndex); + if (info == null) { + if (!ignorePackages.contains(component.getPackageName())) { + + if (mFilterMode == MODE_SET_INVALID_ITEMS) { + mIconCache.remove(component, user); + mItemsToDelete.put(rowId, true); + } + } + continue; + } + if ((info.applicationInfo.flags & ApplicationInfo.FLAG_IS_DATA_ONLY) != 0) { + // Application is not present + continue; + } + + long updateTime = c.getLong(indexLastUpdate); + int version = c.getInt(indexVersion); + T app = componentMap.remove(component); + if (version == info.versionCode && updateTime == info.lastUpdateTime + && TextUtils.equals(c.getString(systemStateIndex), + mIconCache.getIconSystemState(info.packageName))) { + + if (mFilterMode == MODE_CLEAR_VALID_ITEMS) { + mItemsToDelete.put(rowId, false); + } + continue; + } + + if (app == null) { + if (mFilterMode == MODE_SET_INVALID_ITEMS) { + mIconCache.remove(component, user); + mItemsToDelete.put(rowId, true); + } + } else { + appsToUpdate.add(app); + } + } + } catch (SQLiteException e) { + Log.d(TAG, "Error reading icon cache", e); + // Continue updating whatever we have read so far + } + + // Insert remaining apps. + if (!componentMap.isEmpty() || !appsToUpdate.isEmpty()) { + Stack<T> appsToAdd = new Stack<>(); + appsToAdd.addAll(componentMap.values()); + new SerializedIconUpdateTask(userSerial, user, appsToAdd, appsToUpdate, cachingLogic, + onUpdateCallback).scheduleNext(); + } + } + + /** + * Commits all updates as part of the update handler to disk. Not more calls should be made + * to this class after this. + */ + public void finish() { + // Commit all deletes + int deleteCount = 0; + StringBuilder queryBuilder = new StringBuilder() + .append(IconDB.COLUMN_ROWID) + .append(" IN ("); + + int count = mItemsToDelete.size(); + for (int i = 0; i < count; i++) { + if (mItemsToDelete.valueAt(i)) { + if (deleteCount > 0) { + queryBuilder.append(", "); + } + queryBuilder.append(mItemsToDelete.keyAt(i)); + deleteCount++; + } + } + queryBuilder.append(')'); + + if (deleteCount > 0) { + mIconCache.mIconDb.delete(queryBuilder.toString(), null); + } + } + + /** + * A runnable that updates invalid icons and adds missing icons in the DB for the provided + * LauncherActivityInfo list. Items are updated/added one at a time, so that the + * worker thread doesn't get blocked. + */ + private class SerializedIconUpdateTask<T> implements Runnable { + private final long mUserSerial; + private final UserHandle mUserHandle; + private final Stack<T> mAppsToAdd; + private final Stack<T> mAppsToUpdate; + private final CachingLogic<T> mCachingLogic; + private final HashSet<String> mUpdatedPackages = new HashSet<>(); + private final OnUpdateCallback mOnUpdateCallback; + + SerializedIconUpdateTask(long userSerial, UserHandle userHandle, + Stack<T> appsToAdd, Stack<T> appsToUpdate, CachingLogic<T> cachingLogic, + OnUpdateCallback onUpdateCallback) { + mUserHandle = userHandle; + mUserSerial = userSerial; + mAppsToAdd = appsToAdd; + mAppsToUpdate = appsToUpdate; + mCachingLogic = cachingLogic; + mOnUpdateCallback = onUpdateCallback; + } + + @Override + public void run() { + if (!mAppsToUpdate.isEmpty()) { + T app = mAppsToUpdate.pop(); + String pkg = mCachingLogic.getComponent(app).getPackageName(); + PackageInfo info = mPkgInfoMap.get(pkg); + + mIconCache.addIconToDBAndMemCache( + app, mCachingLogic, info, mUserSerial, true /*replace existing*/); + mUpdatedPackages.add(pkg); + + if (mAppsToUpdate.isEmpty() && !mUpdatedPackages.isEmpty()) { + // No more app to update. Notify callback. + mOnUpdateCallback.onPackageIconsUpdated(mUpdatedPackages, mUserHandle); + } + + // Let it run one more time. + scheduleNext(); + } else if (!mAppsToAdd.isEmpty()) { + T app = mAppsToAdd.pop(); + PackageInfo info = mPkgInfoMap.get(mCachingLogic.getComponent(app).getPackageName()); + // We do not check the mPkgInfoMap when generating the mAppsToAdd. Although every + // app should have package info, this is not guaranteed by the api + if (info != null) { + mIconCache.addIconToDBAndMemCache(app, mCachingLogic, info, + mUserSerial, false /*replace existing*/); + } + + if (!mAppsToAdd.isEmpty()) { + scheduleNext(); + } + } + } + + public void scheduleNext() { + mIconCache.mWorkerHandler.postAtTime(this, ICON_UPDATE_TOKEN, + SystemClock.uptimeMillis() + 1); + } + } + + public interface OnUpdateCallback { + + void onPackageIconsUpdated(HashSet<String> updatedPackages, UserHandle user); + } +} diff --git a/PermissionController/iconloaderlib/src/com/android/launcher3/util/ComponentKey.java b/PermissionController/iconloaderlib/src/com/android/launcher3/util/ComponentKey.java new file mode 100644 index 000000000..71451031d --- /dev/null +++ b/PermissionController/iconloaderlib/src/com/android/launcher3/util/ComponentKey.java @@ -0,0 +1,84 @@ +package com.android.launcher3.util; + +/** + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.content.ComponentName; +import android.os.UserHandle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Arrays; + +public class ComponentKey { + + public final ComponentName componentName; + public final UserHandle user; + + private final int mHashCode; + + public ComponentKey(ComponentName componentName, UserHandle user) { + if (componentName == null || user == null) { + throw new NullPointerException(); + } + this.componentName = componentName; + this.user = user; + mHashCode = Arrays.hashCode(new Object[] {componentName, user}); + + } + + @Override + public int hashCode() { + return mHashCode; + } + + @Override + public boolean equals(Object o) { + ComponentKey other = (ComponentKey) o; + return other.componentName.equals(componentName) && other.user.equals(user); + } + + /** + * Encodes a component key as a string of the form [flattenedComponentString#userId]. + */ + @Override + public String toString() { + return componentName.flattenToString() + "#" + user.hashCode(); + } + + /** + * Parses and returns ComponentKey objected from string representation + * Returns null if string is not properly formatted + */ + @Nullable + public static ComponentKey fromString(@NonNull String str) { + int sep = str.indexOf('#'); + if (sep < 0 || (sep + 1) >= str.length()) { + return null; + } + ComponentName componentName = ComponentName.unflattenFromString(str.substring(0, sep)); + if (componentName == null) { + return null; + } + try { + return new ComponentKey(componentName, + UserHandle.getUserHandleForUid(Integer.parseInt(str.substring(sep + 1)))); + } catch (NumberFormatException ex) { + return null; + } + } +}
\ No newline at end of file diff --git a/PermissionController/iconloaderlib/src/com/android/launcher3/util/NoLocaleSQLiteHelper.java b/PermissionController/iconloaderlib/src/com/android/launcher3/util/NoLocaleSQLiteHelper.java new file mode 100644 index 000000000..fe864a284 --- /dev/null +++ b/PermissionController/iconloaderlib/src/com/android/launcher3/util/NoLocaleSQLiteHelper.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.util; + +import static android.database.sqlite.SQLiteDatabase.NO_LOCALIZED_COLLATORS; + +import android.content.Context; +import android.content.ContextWrapper; +import android.database.DatabaseErrorHandler; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteDatabase.CursorFactory; +import android.database.sqlite.SQLiteDatabase.OpenParams; +import android.database.sqlite.SQLiteOpenHelper; +import android.os.Build; + +/** + * Extension of {@link SQLiteOpenHelper} which avoids creating default locale table by + * A context wrapper which creates databases without support for localized collators. + */ +public abstract class NoLocaleSQLiteHelper extends SQLiteOpenHelper { + + private static final boolean ATLEAST_P = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.P; + + public NoLocaleSQLiteHelper(Context context, String name, int version) { + super(ATLEAST_P ? context : new NoLocalContext(context), name, null, version); + if (ATLEAST_P) { + setOpenParams(new OpenParams.Builder().addOpenFlags(NO_LOCALIZED_COLLATORS).build()); + } + } + + private static class NoLocalContext extends ContextWrapper { + public NoLocalContext(Context base) { + super(base); + } + + @Override + public SQLiteDatabase openOrCreateDatabase( + String name, int mode, CursorFactory factory, DatabaseErrorHandler errorHandler) { + return super.openOrCreateDatabase( + name, mode | Context.MODE_NO_LOCALIZED_COLLATORS, factory, errorHandler); + } + } +} diff --git a/PermissionController/iconloaderlib/src/com/android/launcher3/util/SQLiteCacheHelper.java b/PermissionController/iconloaderlib/src/com/android/launcher3/util/SQLiteCacheHelper.java new file mode 100644 index 000000000..49de4bd1b --- /dev/null +++ b/PermissionController/iconloaderlib/src/com/android/launcher3/util/SQLiteCacheHelper.java @@ -0,0 +1,125 @@ +package com.android.launcher3.util; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.database.sqlite.SQLiteFullException; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +/** + * An extension of {@link SQLiteOpenHelper} with utility methods for a single table cache DB. + * Any exception during write operations are ignored, and any version change causes a DB reset. + */ +public abstract class SQLiteCacheHelper { + private static final String TAG = "SQLiteCacheHelper"; + + private static final boolean IN_MEMORY_CACHE = false; + + private final String mTableName; + private final MySQLiteOpenHelper mOpenHelper; + + private boolean mIgnoreWrites; + + public SQLiteCacheHelper(Context context, String name, int version, String tableName) { + if (IN_MEMORY_CACHE) { + name = null; + } + mTableName = tableName; + mOpenHelper = new MySQLiteOpenHelper(context, name, version); + + mIgnoreWrites = false; + } + + /** + * @see SQLiteDatabase#delete(String, String, String[]) + */ + public void delete(String whereClause, String[] whereArgs) { + if (mIgnoreWrites) { + return; + } + try { + mOpenHelper.getWritableDatabase().delete(mTableName, whereClause, whereArgs); + } catch (SQLiteFullException e) { + onDiskFull(e); + } catch (SQLiteException e) { + Log.d(TAG, "Ignoring sqlite exception", e); + } + } + + /** + * @see SQLiteDatabase#insertWithOnConflict(String, String, ContentValues, int) + */ + public void insertOrReplace(ContentValues values) { + if (mIgnoreWrites) { + return; + } + try { + mOpenHelper.getWritableDatabase().insertWithOnConflict( + mTableName, null, values, SQLiteDatabase.CONFLICT_REPLACE); + } catch (SQLiteFullException e) { + onDiskFull(e); + } catch (SQLiteException e) { + Log.d(TAG, "Ignoring sqlite exception", e); + } + } + + private void onDiskFull(SQLiteFullException e) { + Log.e(TAG, "Disk full, all write operations will be ignored", e); + mIgnoreWrites = true; + } + + /** + * @see SQLiteDatabase#query(String, String[], String, String[], String, String, String) + */ + public Cursor query(String[] columns, String selection, String[] selectionArgs) { + return mOpenHelper.getReadableDatabase().query( + mTableName, columns, selection, selectionArgs, null, null, null); + } + + public void clear() { + mOpenHelper.clearDB(mOpenHelper.getWritableDatabase()); + } + + public void close() { + mOpenHelper.close(); + } + + protected abstract void onCreateTable(SQLiteDatabase db); + + /** + * A private inner class to prevent direct DB access. + */ + private class MySQLiteOpenHelper extends NoLocaleSQLiteHelper { + + public MySQLiteOpenHelper(Context context, String name, int version) { + super(context, name, version); + } + + @Override + public void onCreate(SQLiteDatabase db) { + onCreateTable(db); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion != newVersion) { + clearDB(db); + } + } + + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion != newVersion) { + clearDB(db); + } + } + + private void clearDB(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS " + mTableName); + onCreate(db); + } + } +} diff --git a/PermissionController/iconloaderlib/src/com/android/launcher3/util/SafeCloseable.java b/PermissionController/iconloaderlib/src/com/android/launcher3/util/SafeCloseable.java new file mode 100644 index 000000000..ba8ee04d2 --- /dev/null +++ b/PermissionController/iconloaderlib/src/com/android/launcher3/util/SafeCloseable.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.util; + +/** + * Extension of closeable which does not throw an exception + */ +public interface SafeCloseable extends AutoCloseable { + + @Override + void close(); +} diff --git a/PermissionController/iconloaderlib/src_full_lib/com/android/launcher3/icons/IconFactory.java b/PermissionController/iconloaderlib/src_full_lib/com/android/launcher3/icons/IconFactory.java new file mode 100644 index 000000000..48f11fde3 --- /dev/null +++ b/PermissionController/iconloaderlib/src_full_lib/com/android/launcher3/icons/IconFactory.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.icons; + +import android.content.Context; + +/** + * Wrapper class to provide access to {@link BaseIconFactory} and also to provide pool of this class + * that are threadsafe. + */ +public class IconFactory extends BaseIconFactory { + + private static final Object sPoolSync = new Object(); + private static IconFactory sPool; + private static int sPoolId = 0; + + /** + * Return a new Message instance from the global pool. Allows us to + * avoid allocating new objects in many cases. + */ + public static IconFactory obtain(Context context) { + int poolId; + synchronized (sPoolSync) { + if (sPool != null) { + IconFactory m = sPool; + sPool = m.next; + m.next = null; + return m; + } + poolId = sPoolId; + } + + return new IconFactory(context, + context.getResources().getConfiguration().densityDpi, + context.getResources().getDimensionPixelSize(R.dimen.default_icon_bitmap_size), + poolId); + } + + public static void clearPool() { + synchronized (sPoolSync) { + sPool = null; + sPoolId++; + } + } + + private final int mPoolId; + + private IconFactory next; + + private IconFactory(Context context, int fillResIconDpi, int iconBitmapSize, int poolId) { + super(context, fillResIconDpi, iconBitmapSize); + mPoolId = poolId; + } + + /** + * Recycles a LauncherIcons that may be in-use. + */ + public void recycle() { + synchronized (sPoolSync) { + if (sPoolId != mPoolId) { + return; + } + // Clear any temporary state variables + clear(); + + next = sPool; + sPool = this; + } + } + + @Override + public void close() { + recycle(); + } +} diff --git a/PermissionController/iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java b/PermissionController/iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java new file mode 100644 index 000000000..cc4ad7b0b --- /dev/null +++ b/PermissionController/iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.icons; + +import static android.content.Intent.ACTION_MANAGED_PROFILE_ADDED; +import static android.content.Intent.ACTION_MANAGED_PROFILE_REMOVED; + +import android.annotation.TargetApi; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.SparseLongArray; + +import com.android.launcher3.icons.cache.BaseIconCache; + +/** + * Wrapper class to provide access to {@link BaseIconFactory} and also to provide pool of this class + * that are threadsafe. + */ +@TargetApi(Build.VERSION_CODES.P) +public class SimpleIconCache extends BaseIconCache { + + private static SimpleIconCache sIconCache = null; + private static final Object CACHE_LOCK = new Object(); + + private final SparseLongArray mUserSerialMap = new SparseLongArray(2); + private final UserManager mUserManager; + + public SimpleIconCache(Context context, String dbFileName, Looper bgLooper, int iconDpi, + int iconPixelSize, boolean inMemoryCache) { + super(context, dbFileName, bgLooper, iconDpi, iconPixelSize, inMemoryCache); + mUserManager = context.getSystemService(UserManager.class); + + // Listen for user cache changes. + IntentFilter filter = new IntentFilter(ACTION_MANAGED_PROFILE_ADDED); + filter.addAction(ACTION_MANAGED_PROFILE_REMOVED); + context.registerReceiver(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + resetUserCache(); + } + }, filter, null, new Handler(bgLooper), 0); + } + + @Override + protected long getSerialNumberForUser(UserHandle user) { + synchronized (mUserSerialMap) { + int index = mUserSerialMap.indexOfKey(user.getIdentifier()); + if (index >= 0) { + return mUserSerialMap.valueAt(index); + } + long serial = mUserManager.getSerialNumberForUser(user); + mUserSerialMap.put(user.getIdentifier(), serial); + return serial; + } + } + + private void resetUserCache() { + synchronized (mUserSerialMap) { + mUserSerialMap.clear(); + } + } + + @Override + protected boolean isInstantApp(ApplicationInfo info) { + return info.isInstantApp(); + } + + @Override + public BaseIconFactory getIconFactory() { + return IconFactory.obtain(mContext); + } + + public static SimpleIconCache getIconCache(Context context) { + synchronized (CACHE_LOCK) { + if (sIconCache != null) { + return sIconCache; + } + boolean inMemoryCache = + context.getResources().getBoolean(R.bool.simple_cache_enable_im_memory); + String dbFileName = context.getString(R.string.cache_db_name); + + HandlerThread bgThread = new HandlerThread("simple-icon-cache"); + bgThread.start(); + + sIconCache = new SimpleIconCache(context.getApplicationContext(), dbFileName, + bgThread.getLooper(), context.getResources().getConfiguration().densityDpi, + context.getResources().getDimensionPixelSize(R.dimen.default_icon_bitmap_size), + inMemoryCache); + return sIconCache; + } + } +} diff --git a/PermissionController/tests/mocking/Android.bp b/PermissionController/tests/mocking/Android.bp index 89d5626e9..20566764f 100644 --- a/PermissionController/tests/mocking/Android.bp +++ b/PermissionController/tests/mocking/Android.bp @@ -49,7 +49,7 @@ android_test { ], static_libs: [ - "iconloader", + "iconloader_sc_mainline_prod", "com.google.android.material_material", "androidx.transition_transition", "androidx-constraintlayout_constraintlayout", |