diff options
author | Andy Wickham <awickham@google.com> | 2020-03-03 00:33:24 +0000 |
---|---|---|
committer | Andy Wickham <awickham@google.com> | 2020-03-07 10:36:09 +0000 |
commit | 76550b26ba5ac066b12665cc0ec49f7ba93b075e (patch) | |
tree | 7fbb85dc9ed274f1e458bbab3c0960771174570a /iconloaderlib | |
parent | 3b0335ea316cdd7537a37d474522411090540717 (diff) |
Copies iconloaderlib to frameworks/libs/systemui.
Bug: 138964382
Test: N/A
Change-Id: Idf24deb2acd34b38182cc0dcacbfd4163e36b8be
Merged-In: Idf24deb2acd34b38182cc0dcacbfd4163e36b8be
Diffstat (limited to 'iconloaderlib')
27 files changed, 3326 insertions, 0 deletions
diff --git a/iconloaderlib/Android.bp b/iconloaderlib/Android.bp new file mode 100644 index 0000000..f12d16e --- /dev/null +++ b/iconloaderlib/Android.bp @@ -0,0 +1,44 @@ +// 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. + +android_library { + name: "iconloader_base", + sdk_version: "28", + min_sdk_version: "21", + static_libs: [ + "androidx.core_core", + ], + resource_dirs: [ + "res", + ], + srcs: [ + "src/**/*.java", + ], +} + +android_library { + name: "iconloader", + sdk_version: "system_current", + min_sdk_version: "21", + static_libs: [ + "androidx.core_core", + ], + resource_dirs: [ + "res", + ], + srcs: [ + "src/**/*.java", + "src_full_lib/**/*.java", + ], +} diff --git a/iconloaderlib/AndroidManifest.xml b/iconloaderlib/AndroidManifest.xml new file mode 100644 index 0000000..b30258d --- /dev/null +++ b/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/iconloaderlib/IconLoader.iml b/iconloaderlib/IconLoader.iml new file mode 100644 index 0000000..165abaa --- /dev/null +++ b/iconloaderlib/IconLoader.iml @@ -0,0 +1,100 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module external.linked.project.id=":IconLoader" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/../../../../vendor/unbundled_google/packages/SystemUIGoogle/studio-dev/SysUIGradleProject" external.system.id="GRADLE" type="JAVA_MODULE" version="4"> + <component name="FacetManager"> + <facet type="android-gradle" name="Android-Gradle"> + <configuration> + <option name="GRADLE_PROJECT_PATH" value=":IconLoader" /> + <option name="LAST_SUCCESSFUL_SYNC_AGP_VERSION" value="3.3.0" /> + <option name="LAST_KNOWN_AGP_VERSION" value="3.3.0" /> + </configuration> + </facet> + <facet type="android" name="Android"> + <configuration> + <option name="SELECTED_BUILD_VARIANT" value="debug" /> + <option name="ASSEMBLE_TASK_NAME" value="assembleDebug" /> + <option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" /> + <afterSyncTasks> + <task>generateDebugSources</task> + </afterSyncTasks> + <option name="ALLOW_USER_CONFIGURATION" value="false" /> + <option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/res;file://$MODULE_DIR$/build/generated/res/rs/debug;file://$MODULE_DIR$/build/generated/res/resValues/debug" /> + <option name="TEST_RES_FOLDERS_RELATIVE_PATH" value="" /> + <option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" /> + <option name="PROJECT_TYPE" value="1" /> + </configuration> + </facet> + </component> + <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8"> + <output url="file://$MODULE_DIR$/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes" /> + <output-test url="file://$MODULE_DIR$/build/intermediates/javac/debugUnitTest/compileDebugUnitTestJavaWithJavac/classes" /> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/build/generated/source/apt/debug" isTestSource="false" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/aidl_source_output_dir/debug/compileDebugAidl/out" isTestSource="false" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/debug" isTestSource="false" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/renderscript_source_output_dir/debug/compileDebugRenderscript/out" isTestSource="false" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/debug" type="java-resource" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/debug" type="java-resource" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/source/apt/androidTest/debug" isTestSource="true" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/aidl_source_output_dir/debugAndroidTest/compileDebugAndroidTestAidl/out" isTestSource="true" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/debug" isTestSource="true" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/renderscript_source_output_dir/debugAndroidTest/compileDebugAndroidTestRenderscript/out" isTestSource="true" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/debug" type="java-test-resource" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/androidTest/debug" type="java-test-resource" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/source/apt/test/debug" isTestSource="true" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/debug/shaders" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/res" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/resources" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/assets" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/aidl" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/java" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/rs" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/shaders" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/testDebug/res" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/testDebug/resources" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/testDebug/assets" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/testDebug/aidl" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/testDebug/java" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/testDebug/rs" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/testDebug/shaders" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/res" type="java-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src_full_lib" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/main/shaders" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTest/shaders" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/test/res" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/test/assets" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/test/aidl" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/test/rs" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/test/shaders" isTestSource="true" /> + <excludeFolder url="file://$MODULE_DIR$/build" /> + </content> + <orderEntry type="jdk" jdkName="Android API 29 Platform" jdkType="Android SDK" /> + <orderEntry type="sourceFolder" forTests="false" /> + <orderEntry type="library" name="Gradle: androidx.lifecycle:lifecycle-common:2.0.0@jar" level="project" /> + <orderEntry type="library" name="Gradle: androidx.arch.core:core-common:2.0.0@jar" level="project" /> + <orderEntry type="library" name="Gradle: androidx.collection:collection:1.0.0@jar" level="project" /> + <orderEntry type="library" name="Gradle: androidx.annotation:annotation:1.1.0@jar" level="project" /> + <orderEntry type="library" name="Gradle: androidx.core:core:1.3.0-alpha02@aar" level="project" /> + <orderEntry type="library" name="Gradle: androidx.lifecycle:lifecycle-runtime:2.0.0@aar" level="project" /> + <orderEntry type="library" name="Gradle: androidx.versionedparcelable:versionedparcelable:1.1.0@aar" level="project" /> + </component> +</module>
\ No newline at end of file diff --git a/iconloaderlib/build.gradle b/iconloaderlib/build.gradle new file mode 100644 index 0000000..d7a62e1 --- /dev/null +++ b/iconloaderlib/build.gradle @@ -0,0 +1,38 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion COMPILE_SDK + buildToolsVersion BUILD_TOOLS_VERSION + + defaultConfig { + minSdkVersion 25 + 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/iconloaderlib/res/drawable-v26/adaptive_icon_drawable_wrapper.xml b/iconloaderlib/res/drawable-v26/adaptive_icon_drawable_wrapper.xml new file mode 100644 index 0000000..9f13cf5 --- /dev/null +++ b/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/iconloaderlib/res/drawable/ic_instant_app_badge.xml b/iconloaderlib/res/drawable/ic_instant_app_badge.xml new file mode 100644 index 0000000..b74317e --- /dev/null +++ b/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/iconloaderlib/res/values/colors.xml b/iconloaderlib/res/values/colors.xml new file mode 100644 index 0000000..873b2fc --- /dev/null +++ b/iconloaderlib/res/values/colors.xml @@ -0,0 +1,21 @@ +<?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> +</resources> diff --git a/iconloaderlib/res/values/config.xml b/iconloaderlib/res/values/config.xml new file mode 100644 index 0000000..68c2d2e --- /dev/null +++ b/iconloaderlib/res/values/config.xml @@ -0,0 +1,27 @@ +<?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> + +</resources>
\ No newline at end of file diff --git a/iconloaderlib/res/values/dimens.xml b/iconloaderlib/res/values/dimens.xml new file mode 100644 index 0000000..e8c0c44 --- /dev/null +++ b/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/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java new file mode 100644 index 0000000..31a923e --- /dev/null +++ b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java @@ -0,0 +1,396 @@ +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.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.os.Build; +import android.os.Process; +import android.os.UserHandle; + +import androidx.annotation.NonNull; + +/** + * 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; + + 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)); + clear(); + } + + protected 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; + } + + 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)); + } + + 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) + : BitmapInfo.of(bitmap, color); + } + + 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); + 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/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java new file mode 100644 index 0000000..d33f9b1 --- /dev/null +++ b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java @@ -0,0 +1,72 @@ +/* + * 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.Bitmap.Config; + +import androidx.annotation.NonNull; + +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 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; + } + + 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 + */ + default BitmapInfo getExtendedInfo(Bitmap bitmap, int color, BaseIconFactory iconFactory) { + return BitmapInfo.of(bitmap, color); + } + + /** + * Notifies the drawable that it will be drawn directly in the UI, without any preprocessing + */ + default void prepareToDrawOnUi() { } + } +} diff --git a/iconloaderlib/src/com/android/launcher3/icons/BitmapRenderer.java b/iconloaderlib/src/com/android/launcher3/icons/BitmapRenderer.java new file mode 100644 index 0000000..5751ed9 --- /dev/null +++ b/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/iconloaderlib/src/com/android/launcher3/icons/ColorExtractor.java b/iconloaderlib/src/com/android/launcher3/icons/ColorExtractor.java new file mode 100644 index 0000000..87bda82 --- /dev/null +++ b/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/iconloaderlib/src/com/android/launcher3/icons/DotRenderer.java b/iconloaderlib/src/com/android/launcher3/icons/DotRenderer.java new file mode 100644 index 0000000..97a0fd3 --- /dev/null +++ b/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/iconloaderlib/src/com/android/launcher3/icons/FixedScaleDrawable.java b/iconloaderlib/src/com/android/launcher3/icons/FixedScaleDrawable.java new file mode 100644 index 0000000..516965e --- /dev/null +++ b/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/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java b/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java new file mode 100644 index 0000000..22f1f23 --- /dev/null +++ b/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java @@ -0,0 +1,85 @@ +/* + * 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.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.RegionIterator; +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) { + // Try go guesstimate how much space the icon will take when serialized + // to avoid unnecessary allocations/copies during the write (4 bytes per pixel). + int size = bitmap.getWidth() * bitmap.getHeight() * 4; + ByteArrayOutputStream out = new ByteArrayOutputStream(size); + 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; + } + } + + 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(); + } +} diff --git a/iconloaderlib/src/com/android/launcher3/icons/IconNormalizer.java b/iconloaderlib/src/com/android/launcher3/icons/IconNormalizer.java new file mode 100644 index 0000000..de39e79 --- /dev/null +++ b/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/iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java b/iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java new file mode 100644 index 0000000..7702727 --- /dev/null +++ b/iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java @@ -0,0 +1,167 @@ +/* + * 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 float BLUR_FACTOR = 0.5f/48; + + // Percent of actual icon size + public static final float KEY_SHADOW_DISTANCE = 1f/48; + private static final int KEY_SHADOW_ALPHA = 61; + // Percent of actual icon size + private static final float HALF_DISTANCE = 0.5f; + private static final int AMBIENT_SHADOW_ALPHA = 30; + + 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) { + 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; + + // 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) { + shadowBlur = height * 1f / 24; + keyShadowDistance = height * 1f / 16; + 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); + + // 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/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java b/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java new file mode 100644 index 0000000..4c634cb --- /dev/null +++ b/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java @@ -0,0 +1,582 @@ +/* + * 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.BitmapFactory; +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.icons.BitmapRenderer; +import com.android.launcher3.icons.GraphicsUtils; +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 BitmapFactory.Options mDecodeOptions; + 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; + } + }; + } + + if (BitmapRenderer.USE_HARDWARE_BITMAP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + mDecodeOptions = new BitmapFactory.Options(); + mDecodeOptions.inPreferredConfig = Bitmap.Config.HARDWARE; + } else { + mDecodeOptions = null; + } + + 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. + */ + protected 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.createIconBitmap(icon); + 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) { + byte[] data = c.getBlob(2); + try { + entry.bitmap = BitmapInfo.of( + BitmapFactory.decodeByteArray(data, 0, data.length, mDecodeOptions), + entry.bitmap.color); + } catch (Exception e) { } + } + return true; + } + } 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 = 27; + + 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.isLowRes() ? null : GraphicsUtils.flattenBitmap(bitmapInfo.icon)); + 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/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java b/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java new file mode 100644 index 0000000..c12e9dc --- /dev/null +++ b/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/iconloaderlib/src/com/android/launcher3/icons/cache/HandlerRunnable.java b/iconloaderlib/src/com/android/launcher3/icons/cache/HandlerRunnable.java new file mode 100644 index 0000000..ee52934 --- /dev/null +++ b/iconloaderlib/src/com/android/launcher3/icons/cache/HandlerRunnable.java @@ -0,0 +1,67 @@ +/* + * 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; + +/** + * A runnable that can be posted to a {@link Handler} which can be canceled. + */ +public abstract class HandlerRunnable implements Runnable { + + private final Handler mHandler; + private final Runnable mEndRunnable; + + private boolean mEnded = false; + private boolean mCanceled = false; + + public HandlerRunnable(Handler handler, Runnable endRunnable) { + mHandler = handler; + mEndRunnable = endRunnable; + } + + /** + * Cancels this runnable from being run, only if it has not already run. + */ + public void cancel() { + mHandler.removeCallbacks(this); + // TODO: This can actually cause onEnd to be called twice if the handler is already running + // this runnable + // NOTE: This is currently run on whichever thread the caller is run on. + mCanceled = true; + onEnd(); + } + + /** + * @return whether this runnable was canceled. + */ + protected boolean isCanceled() { + return mCanceled; + } + + /** + * To be called by the implemention of this runnable. The end callback is done on whichever + * thread the caller is calling from. + */ + public void onEnd() { + if (!mEnded) { + mEnded = true; + if (mEndRunnable != null) { + mEndRunnable.run(); + } + } + } +} diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java b/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java new file mode 100644 index 0000000..9e1ad7b --- /dev/null +++ b/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/iconloaderlib/src/com/android/launcher3/util/ComponentKey.java b/iconloaderlib/src/com/android/launcher3/util/ComponentKey.java new file mode 100644 index 0000000..34bed94 --- /dev/null +++ b/iconloaderlib/src/com/android/launcher3/util/ComponentKey.java @@ -0,0 +1,59 @@ +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 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; + } +}
\ No newline at end of file diff --git a/iconloaderlib/src/com/android/launcher3/util/NoLocaleSQLiteHelper.java b/iconloaderlib/src/com/android/launcher3/util/NoLocaleSQLiteHelper.java new file mode 100644 index 0000000..fe864a2 --- /dev/null +++ b/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/iconloaderlib/src/com/android/launcher3/util/SQLiteCacheHelper.java b/iconloaderlib/src/com/android/launcher3/util/SQLiteCacheHelper.java new file mode 100644 index 0000000..49de4bd --- /dev/null +++ b/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/iconloaderlib/src_full_lib/com/android/launcher3/icons/IconFactory.java b/iconloaderlib/src_full_lib/com/android/launcher3/icons/IconFactory.java new file mode 100644 index 0000000..48f11fd --- /dev/null +++ b/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/iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java b/iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java new file mode 100644 index 0000000..1337975 --- /dev/null +++ b/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 + protected 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; + } + } +} |