fix: 首次提交

This commit is contained in:
2024-12-09 11:25:23 +08:00
parent d0c01071e9
commit 2c2109a5f3
4741 changed files with 290641 additions and 0 deletions

13
iconloaderlib/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
*.iml
.project
.classpath
.project.properties
gen/
bin/
.idea/
.gradle/
local.properties
gradle/
build/
gradlew*
.DS_Store

52
iconloaderlib/Android.bp Normal file
View File

@@ -0,0 +1,52 @@
// Copyright (C) 2018 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package {
default_applicable_licenses: ["Android-Apache-2.0"],
}
android_library {
name: "iconloader_base",
sdk_version: "current",
min_sdk_version: "26",
static_libs: [
"androidx.core_core",
],
resource_dirs: [
"res",
],
srcs: [
"src/**/*.java",
],
}
android_library {
name: "iconloader",
sdk_version: "system_current",
min_sdk_version: "26",
static_libs: [
"androidx.core_core",
],
resource_dirs: [
"res",
],
srcs: [
"src/**/*.java",
"src_full_lib/**/*.java",
],
apex_available: [
"//apex_available:platform",
"com.android.permission",
],
}

View File

@@ -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>

View File

@@ -0,0 +1,40 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.android.launcher3.icons"
compileSdk 34
defaultConfig {
minSdk 31
targetSdk 34
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
sourceSets {
main {
java.srcDirs = ['src', 'src_full_lib']
manifest.srcFile 'AndroidManifest.xml'
res.srcDirs = ['res']
}
}
lint {
abortOnError false
}
tasks.withType(JavaCompile) {
options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}
dependencies {
// compileOnly files('../libs/framework.jar')
implementation libs.androidx.core.core
}

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2023 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="24"
android:viewportHeight="24"
android:tint="@color/badge_tint_clone">
<group android:scaleX=".6" android:scaleY=".6" android:pivotX="12" android:pivotY="12">
<path
android:pathData="M22,9.5C22,13.642 18.642,17 14.5,17C10.358,17 7,13.642 7,9.5C7,5.358 10.358,2 14.5,2C18.642,2 22,5.358 22,9.5Z"
android:fillColor="#FFFFFFFF"/>
<path
android:pathData="M9.5,20.333C12.722,20.333 15.333,17.722 15.333,14.5C15.333,11.278 12.722,8.667 9.5,8.667C6.278,8.667 3.667,11.278 3.667,14.5C3.667,17.722 6.278,20.333 9.5,20.333ZM9.5,22C13.642,22 17,18.642 17,14.5C17,10.358 13.642,7 9.5,7C5.358,7 2,10.358 2,14.5C2,18.642 5.358,22 9.5,22Z"
android:fillColor="#FFFFFFFF"
android:fillType="evenOdd"/>
</group>
</vector>

View File

@@ -0,0 +1,27 @@
<?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"
android:tint="@color/badge_tint_instant">
<path
android:fillColor="#FFFFFFFF"
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>

View File

@@ -0,0 +1,28 @@
<!--
~ Copyright (C) 2023 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="24"
android:viewportHeight="24"
android:tint="@color/badge_tint_private">
<group android:scaleX=".6" android:scaleY=".6" android:pivotX="12" android:pivotY="12">
<path
android:pathData="M5,3H19C20.1,3 21,3.9 21,5V19C21,20.1 20.1,21 19,21H5C3.9,21 3,20.1 3,19V5C3,3.9 3.9,3 5,3ZM13.5,15.501L12.93,12.271C13.57,11.941 14,11.271 14,10.501C14,9.401 13.1,8.501 12,8.501C10.9,8.501 10,9.401 10,10.501C10,11.271 10.43,11.941 11.07,12.271L10.5,15.501H13.5Z"
android:fillColor="#FFFFFFFF"/>
</group>
</vector>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="@dimen/profile_badge_size"
android:height="@dimen/profile_badge_size"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/badge_tint_work">
<group android:scaleX=".6" android:scaleY=".6" android:pivotX="12" android:pivotY="12">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M20,6h-4L16,4c0,-1.11 -0.89,-2 -2,-2h-4c-1.11,0 -2,0.89 -2,2v2L4,6c-1.11,0 -1.99,0.89 -1.99,2L2,19c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2zM14,6h-4L10,4h4v2z" />
</group>
</vector>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
**
** Copyright 2023, 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="themed_icon_color">@android:color/system_accent1_200</color>
<color name="themed_icon_background_color">@android:color/system_accent2_800</color>
<color name="themed_badge_icon_color">@android:color/system_accent2_800</color>
<color name="themed_badge_icon_background_color">@android:color/system_accent1_200</color>
</resources>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
**
** Copyright 2023, 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="themed_icon_color">#A8C7FA</color>
<color name="themed_icon_background_color">#003355</color>
<color name="themed_badge_icon_color">#003355</color>
<color name="themed_badge_icon_background_color">#A8C7FA</color>
</resources>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
**
** Copyright 2023, 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="themed_icon_color">@android:color/system_accent1_700</color>
<color name="themed_icon_background_color">@android:color/system_accent1_100</color>
<color name="themed_badge_icon_color">@android:color/system_accent1_700</color>
<color name="themed_badge_icon_background_color">@android:color/system_accent1_100</color>
</resources>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
**
** Copyright 2021, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
** http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
-->
<resources>
<attr name="disabledIconAlpha" format="float" />
<attr name="loadingIconColor" format="color" />
</resources>

View File

@@ -0,0 +1,32 @@
<?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="themed_icon_color">#0842A0</color>
<color name="themed_icon_background_color">#D3E3FD</color>
<color name="themed_badge_icon_color">#0842A0</color>
<color name="themed_badge_icon_background_color">#D3E3FD</color>
<color name="badge_tint_instant">@android:color/black</color>
<color name="badge_tint_work">#1A73E8</color>
<color name="badge_tint_private">#3C4043</color>
<color name="badge_tint_clone">#ff3C4043</color>
<!-- Yellow 600, used for highlighting "important" conversations in settings & notifications -->
<color name="important_conversation">#f9ab00</color>
</resources>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
**
** Copyright 2018, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
** http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
-->
<resources>
<!-- Various configurations to control the simple cache implementation -->
<dimen name="default_icon_bitmap_size">56dp</dimen>
<bool name="simple_cache_enable_im_memory">false</bool>
<string name="cache_db_name" translatable="false">app_icons.db</string>
<string name="calendar_component_name" translatable="false"></string>
<string name="clock_component_name" translatable="false"></string>
</resources>

View File

@@ -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>

View File

@@ -0,0 +1,679 @@
package com.android.launcher3.icons;
import static android.graphics.Paint.ANTI_ALIAS_FLAG;
import static android.graphics.Paint.DITHER_FLAG;
import static android.graphics.Paint.FILTER_BITMAP_FLAG;
import static android.graphics.drawable.AdaptiveIconDrawable.getExtraInsetFraction;
import static com.android.launcher3.icons.BitmapInfo.FLAG_CLONE;
import static com.android.launcher3.icons.BitmapInfo.FLAG_INSTANT;
import static com.android.launcher3.icons.BitmapInfo.FLAG_PRIVATE;
import static com.android.launcher3.icons.BitmapInfo.FLAG_WORK;
import static com.android.launcher3.icons.ShadowGenerator.BLUR_FACTOR;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.annotation.TargetApi;
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.Bitmap.Config;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PaintFlagsDrawFilter;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.DrawableWrapper;
import android.graphics.drawable.InsetDrawable;
import android.os.Build;
import android.os.UserHandle;
import android.util.SparseArray;
import androidx.annotation.ColorInt;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.icons.BitmapInfo.Extender;
import com.android.launcher3.util.FlagOp;
import com.android.launcher3.util.UserIconInfo;
import java.lang.annotation.Retention;
import java.util.Objects;
/**
* 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 int DEFAULT_WRAPPER_BACKGROUND = Color.WHITE;
private static final float LEGACY_ICON_SCALE = .7f * (1f / (1 + 2 * getExtraInsetFraction()));
public static final int MODE_DEFAULT = 0;
public static final int MODE_ALPHA = 1;
public static final int MODE_WITH_SHADOW = 2;
public static final int MODE_HARDWARE = 3;
public static final int MODE_HARDWARE_WITH_SHADOW = 4;
@Retention(SOURCE)
@IntDef({MODE_DEFAULT, MODE_ALPHA, MODE_WITH_SHADOW, MODE_HARDWARE_WITH_SHADOW, MODE_HARDWARE})
@interface BitmapGenerationMode {}
private static final float ICON_BADGE_SCALE = 0.444f;
@NonNull
private final Rect mOldBounds = new Rect();
@NonNull
private final SparseArray<UserIconInfo> mCachedUserInfo = new SparseArray<>();
@NonNull
protected final Context mContext;
@NonNull
private final Canvas mCanvas;
@NonNull
private final PackageManager mPm;
@NonNull
private final ColorExtractor mColorExtractor;
protected final int mFillResIconDpi;
protected final int mIconBitmapSize;
protected boolean mMonoIconEnabled;
@Nullable
private IconNormalizer mNormalizer;
@Nullable
private ShadowGenerator mShadowGenerator;
private final boolean mShapeDetection;
// Shadow bitmap used as background for theme icons
private Bitmap mWhiteShadowLayer;
private Drawable mWrapperIcon;
private int mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND;
private static int PLACEHOLDER_BACKGROUND_COLOR = Color.rgb(245, 245, 245);
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();
}
public BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize) {
this(context, fillResIconDpi, iconBitmapSize, false);
}
protected void clear() {
mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND;
}
@NonNull
public ShadowGenerator getShadowGenerator() {
if (mShadowGenerator == null) {
mShadowGenerator = new ShadowGenerator(mIconBitmapSize);
}
return mShadowGenerator;
}
@NonNull
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));
}
} catch (Exception e) {
// Icon not found.
}
return null;
}
/**
* Create a placeholder icon using the passed in text.
*
* @param placeholder used for foreground element in the icon bitmap
* @param color used for the foreground text color
* @return
*/
public BitmapInfo createIconBitmap(String placeholder, int color) {
AdaptiveIconDrawable drawable = new AdaptiveIconDrawable(
new ColorDrawable(PLACEHOLDER_BACKGROUND_COLOR),
new CenterTextDrawable(placeholder, color));
Bitmap icon = createIconBitmap(drawable, IconNormalizer.ICON_VISIBLE_AREA_FACTOR);
return BitmapInfo.of(icon, color);
}
public BitmapInfo createIconBitmap(Bitmap icon) {
if (mIconBitmapSize != icon.getWidth() || mIconBitmapSize != icon.getHeight()) {
icon = createIconBitmap(new BitmapDrawable(mContext.getResources(), icon), 1f);
}
return BitmapInfo.of(icon, mColorExtractor.findDominantColorByHue(icon));
}
/**
* Creates an icon from the bitmap cropped to the current device icon shape
*/
@NonNull
public BitmapInfo createShapedIconBitmap(Bitmap icon, IconOptions options) {
Drawable d = new FixedSizeBitmapDrawable(icon);
float inset = getExtraInsetFraction();
inset = inset / (1 + 2 * inset);
d = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK),
new InsetDrawable(d, inset, inset, inset, inset));
return createBadgedIconBitmap(d, options);
}
@NonNull
public BitmapInfo createBadgedIconBitmap(@NonNull Drawable icon) {
return createBadgedIconBitmap(icon, null);
}
/**
* 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
* @return a bitmap suitable for disaplaying as an icon at various system UIs.
*/
@TargetApi(Build.VERSION_CODES.TIRAMISU)
@NonNull
public BitmapInfo createBadgedIconBitmap(@NonNull Drawable icon,
@Nullable IconOptions options) {
boolean shrinkNonAdaptiveIcons = options == null || options.mShrinkNonAdaptiveIcons;
float[] scale = new float[1];
icon = normalizeAndWrapToAdaptiveIcon(icon, shrinkNonAdaptiveIcons, null, scale);
Bitmap bitmap = createIconBitmap(icon, scale[0],
options == null ? MODE_WITH_SHADOW : options.mGenerationMode);
int color = (options != null && options.mExtractedColor != null)
? options.mExtractedColor : mColorExtractor.findDominantColorByHue(bitmap);
BitmapInfo info = BitmapInfo.of(bitmap, color);
if (icon instanceof BitmapInfo.Extender) {
info = ((BitmapInfo.Extender) icon).getExtendedInfo(bitmap, color, this, scale[0]);
} else if (IconProvider.ATLEAST_T && mMonoIconEnabled) {
Drawable mono = getMonochromeDrawable(icon);
if (mono != null) {
info.setMonoIcon(createIconBitmap(mono, scale[0], MODE_ALPHA), this);
}
}
info = info.withFlags(getBitmapFlagOp(options));
return info;
}
/**
* Returns a monochromatic version of the given drawable or null, if it is not supported
* @param base the original icon
*/
@TargetApi(Build.VERSION_CODES.TIRAMISU)
protected Drawable getMonochromeDrawable(Drawable base) {
if (base instanceof AdaptiveIconDrawable) {
Drawable mono = ((AdaptiveIconDrawable) base).getMonochrome();
if (mono != null) {
return new ClippedMonoDrawable(mono);
}
}
return null;
}
@NonNull
public FlagOp getBitmapFlagOp(@Nullable IconOptions options) {
FlagOp op = FlagOp.NO_OP;
if (options != null) {
if (options.mIsInstantApp) {
op = op.addFlag(FLAG_INSTANT);
}
UserIconInfo info = options.mUserIconInfo;
if (info == null && options.mUserHandle != null) {
info = getUserInfo(options.mUserHandle);
}
if (info != null) {
op = info.applyBitmapInfoFlags(op);
}
}
return op;
}
@NonNull
protected UserIconInfo getUserInfo(@NonNull UserHandle user) {
int key = user.hashCode();
UserIconInfo info = mCachedUserInfo.get(key);
/*
* We do not have the ability to distinguish between different badged users here.
* As such all badged users will have the work profile badge applied.
*/
if (info == null) {
// Simple check to check if the provided user is work profile or not based on badging
NoopDrawable d = new NoopDrawable();
boolean isWork = (d != mPm.getUserBadgedIcon(d, user));
info = new UserIconInfo(user, isWork ? UserIconInfo.TYPE_WORK : UserIconInfo.TYPE_MAIN);
mCachedUserInfo.put(key, info);
}
return info;
}
@NonNull
public Bitmap getWhiteShadowLayer() {
if (mWhiteShadowLayer == null) {
mWhiteShadowLayer = createScaledBitmap(
new AdaptiveIconDrawable(new ColorDrawable(Color.WHITE), null),
MODE_HARDWARE_WITH_SHADOW);
}
return mWhiteShadowLayer;
}
@NonNull
public Bitmap createScaledBitmap(@NonNull Drawable icon, @BitmapGenerationMode int mode) {
RectF iconBounds = new RectF();
float[] scale = new float[1];
icon = normalizeAndWrapToAdaptiveIcon(icon, true, iconBounds, scale);
return createIconBitmap(icon,
Math.min(scale[0], ShadowGenerator.getScaleForBounds(iconBounds)), mode);
}
/**
* Sets the background color used for wrapped adaptive icon
*/
public void setWrapperBackgroundColor(final int color) {
mWrapperBackgroundColor = (Color.alpha(color) < 255) ? DEFAULT_WRAPPER_BACKGROUND : color;
}
@Nullable
protected Drawable normalizeAndWrapToAdaptiveIcon(@Nullable Drawable icon,
final boolean shrinkNonAdaptiveIcons, @Nullable final RectF outIconBounds,
@NonNull final float[] outScale) {
if (icon == null) {
return null;
}
float scale;
if (shrinkNonAdaptiveIcons && !(icon instanceof AdaptiveIconDrawable)) {
EmptyWrapper foreground = new EmptyWrapper();
AdaptiveIconDrawable dr = new AdaptiveIconDrawable(
new ColorDrawable(mWrapperBackgroundColor), foreground);
dr.setBounds(0, 0, 1, 1);
boolean[] outShape = new boolean[1];
scale = getNormalizer().getScale(icon, outIconBounds, dr.getIconMask(), outShape);
if (!outShape[0]) {
foreground.setDrawable(createScaledDrawable(icon, scale * LEGACY_ICON_SCALE));
icon = dr;
scale = getNormalizer().getScale(icon, outIconBounds, null, null);
}
} else {
scale = getNormalizer().getScale(icon, outIconBounds, null, null);
}
outScale[0] = scale;
return icon;
}
/**
* Returns a drawable which draws the original drawable at a fixed scale
*/
private Drawable createScaledDrawable(@NonNull Drawable main, float scale) {
float h = main.getIntrinsicHeight();
float w = main.getIntrinsicWidth();
float scaleX = scale;
float scaleY = scale;
if (h > w && w > 0) {
scaleX *= w / h;
} else if (w > h && h > 0) {
scaleY *= h / w;
}
scaleX = (1 - scaleX) / 2;
scaleY = (1 - scaleY) / 2;
return new InsetDrawable(main, scaleX, scaleY, scaleX, scaleY);
}
/**
* Wraps the provided icon in an adaptive icon drawable
*/
public AdaptiveIconDrawable wrapToAdaptiveIcon(@NonNull Drawable icon) {
if (icon instanceof AdaptiveIconDrawable aid) {
return aid;
} else {
EmptyWrapper foreground = new EmptyWrapper();
AdaptiveIconDrawable dr = new AdaptiveIconDrawable(
new ColorDrawable(mWrapperBackgroundColor), foreground);
dr.setBounds(0, 0, 1, 1);
boolean[] outShape = new boolean[1];
float scale = getNormalizer().getScale(icon, null, dr.getIconMask(), outShape);
if (!outShape[0]) {
foreground.setDrawable(createScaledDrawable(icon, scale * LEGACY_ICON_SCALE));
} else {
foreground.setDrawable(createScaledDrawable(icon, 1 - getExtraInsetFraction()));
}
return dr;
}
}
@NonNull
protected Bitmap createIconBitmap(@Nullable final Drawable icon, final float scale) {
return createIconBitmap(icon, scale, MODE_DEFAULT);
}
@NonNull
protected Bitmap createIconBitmap(@Nullable final Drawable icon, final float scale,
@BitmapGenerationMode int bitmapGenerationMode) {
final int size = mIconBitmapSize;
final Bitmap bitmap;
switch (bitmapGenerationMode) {
case MODE_ALPHA:
bitmap = Bitmap.createBitmap(size, size, Config.ALPHA_8);
break;
case MODE_HARDWARE:
case MODE_HARDWARE_WITH_SHADOW: {
return BitmapRenderer.createHardwareBitmap(size, size, canvas ->
drawIconBitmap(canvas, icon, scale, bitmapGenerationMode, null));
}
case MODE_WITH_SHADOW:
default:
bitmap = Bitmap.createBitmap(size, size, Config.ARGB_8888);
break;
}
if (icon == null) {
return bitmap;
}
mCanvas.setBitmap(bitmap);
drawIconBitmap(mCanvas, icon, scale, bitmapGenerationMode, bitmap);
mCanvas.setBitmap(null);
return bitmap;
}
private void drawIconBitmap(@NonNull Canvas canvas, @Nullable final Drawable icon,
final float scale, @BitmapGenerationMode int bitmapGenerationMode,
@Nullable Bitmap targetBitmap) {
final int size = mIconBitmapSize;
mOldBounds.set(icon.getBounds());
if (icon instanceof AdaptiveIconDrawable) {
// We are ignoring KEY_SHADOW_DISTANCE because regular icons ignore this at the moment b/298203449
int offset = Math.max((int) Math.ceil(BLUR_FACTOR * size),
Math.round(size * (1 - scale) / 2));
// b/211896569: AdaptiveIconDrawable do not work properly for non top-left bounds
icon.setBounds(0, 0, size - offset - offset, size - offset - offset);
int count = canvas.save();
canvas.translate(offset, offset);
if (bitmapGenerationMode == MODE_WITH_SHADOW
|| bitmapGenerationMode == MODE_HARDWARE_WITH_SHADOW) {
getShadowGenerator().addPathShadow(
((AdaptiveIconDrawable) icon).getIconMask(), canvas);
}
if (icon instanceof BitmapInfo.Extender) {
((Extender) icon).drawForPersistence(canvas);
} else {
icon.draw(canvas);
}
canvas.restoreToCount(count);
} else {
if (icon instanceof BitmapDrawable) {
BitmapDrawable bitmapDrawable = (BitmapDrawable) icon;
Bitmap b = bitmapDrawable.getBitmap();
if (b != 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);
canvas.save();
canvas.scale(scale, scale, size / 2, size / 2);
icon.draw(canvas);
canvas.restore();
if (bitmapGenerationMode == MODE_WITH_SHADOW && targetBitmap != null) {
// Shadow extraction only works in software mode
getShadowGenerator().drawShadow(targetBitmap, canvas);
// Draw the icon again on top:
canvas.save();
canvas.scale(scale, scale, size / 2, size / 2);
icon.draw(canvas);
canvas.restore();
}
}
icon.setBounds(mOldBounds);
}
@Override
public void close() {
clear();
}
@NonNull
public BitmapInfo makeDefaultIcon() {
return createBadgedIconBitmap(getFullResDefaultActivityIcon(mFillResIconDpi));
}
@NonNull
public static Drawable getFullResDefaultActivityIcon(final int iconDpi) {
return Objects.requireNonNull(Resources.getSystem().getDrawableForDensity(
android.R.drawable.sym_def_app_icon, iconDpi));
}
/**
* Returns the correct badge size given an icon size
*/
public static int getBadgeSizeForIconSize(final int iconSize) {
return (int) (ICON_BADGE_SCALE * iconSize);
}
public static class IconOptions {
boolean mShrinkNonAdaptiveIcons = true;
boolean mIsInstantApp;
@BitmapGenerationMode
int mGenerationMode = MODE_WITH_SHADOW;
@Nullable UserHandle mUserHandle;
@Nullable
UserIconInfo mUserIconInfo;
@ColorInt
@Nullable Integer mExtractedColor;
/**
* Set to false if non-adaptive icons should not be treated
*/
@NonNull
public IconOptions setShrinkNonAdaptiveIcons(final boolean shrink) {
mShrinkNonAdaptiveIcons = shrink;
return this;
}
/**
* User for this icon, in case of badging
*/
@NonNull
public IconOptions setUser(@Nullable final UserHandle user) {
mUserHandle = user;
return this;
}
/**
* User for this icon, in case of badging
*/
@NonNull
public IconOptions setUser(@Nullable final UserIconInfo user) {
mUserIconInfo = user;
return this;
}
/**
* If this icon represents an instant app
*/
@NonNull
public IconOptions setInstantApp(final boolean instantApp) {
mIsInstantApp = instantApp;
return this;
}
/**
* Disables auto color extraction and overrides the color to the provided value
*/
@NonNull
public IconOptions setExtractedColor(@ColorInt int color) {
mExtractedColor = color;
return this;
}
/**
* Sets the bitmap generation mode to use for the bitmap info. Note that some generation
* modes do not support color extraction, so consider setting a extracted color manually
* in those cases.
*/
public IconOptions setBitmapGenerationMode(@BitmapGenerationMode int generationMode) {
mGenerationMode = generationMode;
return this;
}
}
/**
* 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(@Nullable final Bitmap bitmap) {
super(null, bitmap);
}
@Override
public int getIntrinsicHeight() {
return getBitmap().getWidth();
}
@Override
public int getIntrinsicWidth() {
return getBitmap().getWidth();
}
}
private static class NoopDrawable extends ColorDrawable {
@Override
public int getIntrinsicHeight() {
return 1;
}
@Override
public int getIntrinsicWidth() {
return 1;
}
}
protected static class ClippedMonoDrawable extends InsetDrawable {
@NonNull
private final AdaptiveIconDrawable mCrop;
public ClippedMonoDrawable(@Nullable final Drawable base) {
super(base, -getExtraInsetFraction());
mCrop = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null);
}
@Override
public void draw(Canvas canvas) {
mCrop.setBounds(getBounds());
int saveCount = canvas.save();
canvas.clipPath(mCrop.getIconMask());
super.draw(canvas);
canvas.restoreToCount(saveCount);
}
}
private static class CenterTextDrawable extends ColorDrawable {
@NonNull
private final Rect mTextBounds = new Rect();
@NonNull
private final Paint mTextPaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG);
@NonNull
private final String mText;
CenterTextDrawable(@NonNull final String text, final int color) {
mText = text;
mTextPaint.setColor(color);
}
@Override
public void draw(Canvas canvas) {
Rect bounds = getBounds();
mTextPaint.setTextSize(bounds.height() / 3f);
mTextPaint.getTextBounds(mText, 0, mText.length(), mTextBounds);
canvas.drawText(mText,
bounds.exactCenterX() - mTextBounds.exactCenterX(),
bounds.exactCenterY() - mTextBounds.exactCenterY(),
mTextPaint);
}
}
private static class EmptyWrapper extends DrawableWrapper {
EmptyWrapper() {
super(new ColorDrawable());
}
@Override
public ConstantState getConstantState() {
Drawable d = getDrawable();
return d == null ? null : d.getConstantState();
}
}
}

View File

@@ -0,0 +1,226 @@
/*
* 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.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.util.FlagOp;
public class BitmapInfo {
public static final int FLAG_WORK = 1 << 0;
public static final int FLAG_INSTANT = 1 << 1;
public static final int FLAG_CLONE = 1 << 2;
public static final int FLAG_PRIVATE = 1 << 3;
@IntDef(flag = true, value = {
FLAG_WORK,
FLAG_INSTANT,
FLAG_CLONE,
FLAG_PRIVATE
})
@interface BitmapInfoFlags {}
public static final int FLAG_THEMED = 1 << 0;
public static final int FLAG_NO_BADGE = 1 << 1;
public static final int FLAG_SKIP_USER_BADGE = 1 << 2;
@IntDef(flag = true, value = {
FLAG_THEMED,
FLAG_NO_BADGE,
FLAG_SKIP_USER_BADGE,
})
public @interface DrawableCreationFlags {}
public static final Bitmap LOW_RES_ICON = Bitmap.createBitmap(1, 1, Config.ALPHA_8);
public static final BitmapInfo LOW_RES_INFO = fromBitmap(LOW_RES_ICON);
public static final String TAG = "BitmapInfo";
public final Bitmap icon;
public final int color;
@Nullable
protected Bitmap mMono;
protected Bitmap mWhiteShadowLayer;
public @BitmapInfoFlags int flags;
private BitmapInfo badgeInfo;
public BitmapInfo(Bitmap icon, int color) {
this.icon = icon;
this.color = color;
}
public BitmapInfo withBadgeInfo(BitmapInfo badgeInfo) {
BitmapInfo result = clone();
result.badgeInfo = badgeInfo;
return result;
}
/**
* Returns a bitmapInfo with the flagOP applied
*/
public BitmapInfo withFlags(@NonNull FlagOp op) {
if (op == FlagOp.NO_OP) {
return this;
}
BitmapInfo result = clone();
result.flags = op.apply(result.flags);
return result;
}
protected BitmapInfo copyInternalsTo(BitmapInfo target) {
target.mMono = mMono;
target.mWhiteShadowLayer = mWhiteShadowLayer;
target.flags = flags;
target.badgeInfo = badgeInfo;
return target;
}
@Override
public BitmapInfo clone() {
return copyInternalsTo(new BitmapInfo(icon, color));
}
public void setMonoIcon(Bitmap mono, BaseIconFactory iconFactory) {
mMono = mono;
mWhiteShadowLayer = iconFactory.getWhiteShadowLayer();
}
/**
* 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;
}
/**
* BitmapInfo can be stored on disk or other persistent storage
*/
public boolean canPersist() {
return !isNullOrLowRes();
}
public Bitmap getMono() {
return mMono;
}
/**
* Creates a drawable for the provided BitmapInfo
*/
public FastBitmapDrawable newIcon(Context context) {
return newIcon(context, 0);
}
/**
* Creates a drawable for the provided BitmapInfo
*/
public FastBitmapDrawable newIcon(Context context, @DrawableCreationFlags int creationFlags) {
FastBitmapDrawable drawable;
if (isLowRes()) {
drawable = new PlaceHolderIconDrawable(this, context);
} else if ((creationFlags & FLAG_THEMED) != 0 && mMono != null) {
drawable = ThemedIconDrawable.newDrawable(this, context);
} else {
drawable = new FastBitmapDrawable(this);
}
applyFlags(context, drawable, creationFlags);
return drawable;
}
protected void applyFlags(Context context, FastBitmapDrawable drawable,
@DrawableCreationFlags int creationFlags) {
drawable.mDisabledAlpha = GraphicsUtils.getFloat(context, R.attr.disabledIconAlpha, 1f);
drawable.mCreationFlags = creationFlags;
if ((creationFlags & FLAG_NO_BADGE) == 0) {
Drawable badge = getBadgeDrawable(context, (creationFlags & FLAG_THEMED) != 0,
(creationFlags & FLAG_SKIP_USER_BADGE) != 0);
if (badge != null) {
drawable.setBadge(badge);
}
}
}
public Drawable getBadgeDrawable(Context context, boolean isThemed) {
return getBadgeDrawable(context, isThemed, false);
}
/**
* Returns a drawable representing the badge for this info
*/
@Nullable
private Drawable getBadgeDrawable(Context context, boolean isThemed, boolean skipUserBadge) {
if (badgeInfo != null) {
int creationFlag = isThemed ? FLAG_THEMED : 0;
if (skipUserBadge) {
creationFlag |= FLAG_SKIP_USER_BADGE;
}
return badgeInfo.newIcon(context, creationFlag);
}
if (skipUserBadge) {
return null;
} else if ((flags & FLAG_INSTANT) != 0) {
return new UserBadgeDrawable(context, R.drawable.ic_instant_app_badge,
R.color.badge_tint_instant, isThemed);
} else if ((flags & FLAG_WORK) != 0) {
return new UserBadgeDrawable(context, R.drawable.ic_work_app_badge,
R.color.badge_tint_work, isThemed);
} else if ((flags & FLAG_CLONE) != 0) {
return new UserBadgeDrawable(context, R.drawable.ic_clone_app_badge,
R.color.badge_tint_clone, isThemed);
} else if ((flags & FLAG_PRIVATE) != 0) {
return new UserBadgeDrawable(context, R.drawable.ic_private_profile_app_badge,
R.color.badge_tint_private, isThemed);
}
return null;
}
public static BitmapInfo fromBitmap(@NonNull Bitmap bitmap) {
return of(bitmap, 0);
}
public static BitmapInfo of(@NonNull Bitmap bitmap, int color) {
return new BitmapInfo(bitmap, color);
}
/**
* Interface to be implemented by drawables to provide a custom BitmapInfo
*/
public interface Extender {
/**
* Called for creating a custom BitmapInfo
*/
BitmapInfo getExtendedInfo(Bitmap bitmap, int color,
BaseIconFactory iconFactory, float normalizationScale);
/**
* Called to draw the UI independent of any runtime configurations like time or theme
*/
void drawForPersistence(Canvas canvas);
}
}

View File

@@ -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);
}

View File

@@ -0,0 +1,160 @@
package com.android.launcher3.icons;
import android.content.Context;
import android.content.Intent;
import android.content.pm.LauncherApps;
import android.content.pm.ShortcutInfo;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* Factory for creating normalized bubble icons and app badges.
*/
public class BubbleIconFactory extends BaseIconFactory {
private final int mRingColor;
private final int mRingWidth;
private final BaseIconFactory mBadgeFactory;
/**
* Creates a bubble icon factory.
*
* @param context the context for the factory.
* @param iconSize the size of the bubble icon (i.e. the large icon for the bubble).
* @param badgeSize the size of the badge (i.e. smaller icon shown on top of the large icon).
* @param ringColor the color of the ring optionally shown around the badge.
* @param ringWidth the width of the ring optionally shown around the badge.
*/
public BubbleIconFactory(Context context, int iconSize, int badgeSize, int ringColor,
int ringWidth) {
super(context, context.getResources().getConfiguration().densityDpi, iconSize);
mRingColor = ringColor;
mRingWidth = ringWidth;
mBadgeFactory = new BaseIconFactory(context,
context.getResources().getConfiguration().densityDpi,
badgeSize);
}
/**
* Returns the drawable that the developer has provided to display in the bubble.
*/
public Drawable getBubbleDrawable(@NonNull final Context context,
@Nullable final ShortcutInfo shortcutInfo, @Nullable final Icon ic) {
if (shortcutInfo != null) {
LauncherApps launcherApps = context.getSystemService(LauncherApps.class);
int density = context.getResources().getConfiguration().densityDpi;
return launcherApps.getShortcutIconDrawable(shortcutInfo, density);
} else {
if (ic != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (ic.getType() == Icon.TYPE_URI
|| ic.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP) {
context.grantUriPermission(context.getPackageName(),
ic.getUri(),
Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
return ic.loadDrawable(context);
}
return null;
}
}
/**
* Creates the bitmap for the provided drawable and returns the scale used for
* drawing the actual drawable. This is used for the larger icon shown for the bubble.
*/
public Bitmap getBubbleBitmap(@NonNull Drawable icon, float[] outScale) {
if (outScale == null) {
outScale = new float[1];
}
icon = normalizeAndWrapToAdaptiveIcon(icon,
true /* shrinkNonAdaptiveIcons */,
null /* outscale */,
outScale);
return createIconBitmap(icon, outScale[0], MODE_WITH_SHADOW);
}
/**
* Returns a {@link BitmapInfo} for the app-badge that is shown on top of each bubble. This
* will include the workprofile indicator on the badge if appropriate.
*/
public BitmapInfo getBadgeBitmap(Drawable userBadgedAppIcon, boolean isImportantConversation) {
if (userBadgedAppIcon instanceof AdaptiveIconDrawable) {
AdaptiveIconDrawable ad = (AdaptiveIconDrawable) userBadgedAppIcon;
userBadgedAppIcon = new CircularAdaptiveIcon(ad.getBackground(),
ad.getForeground());
}
if (isImportantConversation) {
userBadgedAppIcon = new CircularRingDrawable(userBadgedAppIcon);
}
Bitmap userBadgedBitmap = mBadgeFactory.createIconBitmap(
userBadgedAppIcon, 1, MODE_WITH_SHADOW);
return mBadgeFactory.createIconBitmap(userBadgedBitmap);
}
private class CircularRingDrawable extends CircularAdaptiveIcon {
final Rect mInnerBounds = new Rect();
final Drawable mDr;
CircularRingDrawable(Drawable dr) {
super(null, null);
mDr = dr;
}
@Override
public void draw(Canvas canvas) {
int save = canvas.save();
canvas.clipPath(getIconMask());
canvas.drawColor(mRingColor);
mInnerBounds.set(getBounds());
mInnerBounds.inset(mRingWidth, mRingWidth);
canvas.translate(mInnerBounds.left, mInnerBounds.top);
mDr.setBounds(0, 0, mInnerBounds.width(), mInnerBounds.height());
mDr.draw(canvas);
canvas.restoreToCount(save);
}
}
private static class CircularAdaptiveIcon extends AdaptiveIconDrawable {
final Path mPath = new Path();
CircularAdaptiveIcon(Drawable bg, Drawable fg) {
super(bg, fg);
}
@Override
public Path getIconMask() {
mPath.reset();
Rect bounds = getBounds();
mPath.addOval(bounds.left, bounds.top, bounds.right, bounds.bottom, Path.Direction.CW);
return mPath;
}
@Override
public void draw(Canvas canvas) {
int save = canvas.save();
canvas.clipPath(getIconMask());
Drawable d;
if ((d = getBackground()) != null) {
d.draw(canvas);
}
if ((d = getForeground()) != null) {
d.draw(canvas);
}
canvas.restoreToCount(save);
}
}
}

View File

@@ -0,0 +1,530 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.icons;
import static com.android.launcher3.icons.IconProvider.ATLEAST_T;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BlendMode;
import android.graphics.BlendModeColorFilter;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Build;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.Log;
import android.util.TypedValue;
import androidx.annotation.Nullable;
import com.android.launcher3.icons.IconProvider.ThemeData;
import java.util.Calendar;
import java.util.concurrent.TimeUnit;
import java.util.function.IntFunction;
/**
* Wrapper over {@link AdaptiveIconDrawable} to intercept icon flattening logic for dynamic
* clock icons
*/
@TargetApi(Build.VERSION_CODES.O)
public class ClockDrawableWrapper extends AdaptiveIconDrawable implements BitmapInfo.Extender {
public static boolean sRunningInTest = false;
private static final String TAG = "ClockDrawableWrapper";
private static final boolean DISABLE_SECONDS = true;
private static final int NO_COLOR = -1;
// Time after which the clock icon should check for an update. The actual invalidate
// will only happen in case of any change.
public static final long TICK_MS = DISABLE_SECONDS ? TimeUnit.MINUTES.toMillis(1) : 200L;
private static final String LAUNCHER_PACKAGE = "com.android.launcher3";
private static final String ROUND_ICON_METADATA_KEY = LAUNCHER_PACKAGE
+ ".LEVEL_PER_TICK_ICON_ROUND";
private static final String HOUR_INDEX_METADATA_KEY = LAUNCHER_PACKAGE + ".HOUR_LAYER_INDEX";
private static final String MINUTE_INDEX_METADATA_KEY = LAUNCHER_PACKAGE
+ ".MINUTE_LAYER_INDEX";
private static final String SECOND_INDEX_METADATA_KEY = LAUNCHER_PACKAGE
+ ".SECOND_LAYER_INDEX";
private static final String DEFAULT_HOUR_METADATA_KEY = LAUNCHER_PACKAGE
+ ".DEFAULT_HOUR";
private static final String DEFAULT_MINUTE_METADATA_KEY = LAUNCHER_PACKAGE
+ ".DEFAULT_MINUTE";
private static final String DEFAULT_SECOND_METADATA_KEY = LAUNCHER_PACKAGE
+ ".DEFAULT_SECOND";
/* Number of levels to jump per second for the second hand */
private static final int LEVELS_PER_SECOND = 10;
public static final int INVALID_VALUE = -1;
private final AnimationInfo mAnimationInfo = new AnimationInfo();
private AnimationInfo mThemeInfo = null;
private ClockDrawableWrapper(AdaptiveIconDrawable base) {
super(base.getBackground(), base.getForeground());
}
private void applyThemeData(ThemeData themeData) {
if (!IconProvider.ATLEAST_T || mThemeInfo != null) {
return;
}
try {
TypedArray ta = themeData.mResources.obtainTypedArray(themeData.mResID);
int count = ta.length();
Bundle extras = new Bundle();
for (int i = 0; i < count; i += 2) {
TypedValue v = ta.peekValue(i + 1);
extras.putInt(ta.getString(i), v.type >= TypedValue.TYPE_FIRST_INT
&& v.type <= TypedValue.TYPE_LAST_INT
? v.data : v.resourceId);
}
ta.recycle();
ClockDrawableWrapper drawable = ClockDrawableWrapper.forExtras(extras, resId -> {
Drawable bg = new ColorDrawable(Color.WHITE);
Drawable fg = themeData.mResources.getDrawable(resId).mutate();
return new AdaptiveIconDrawable(bg, fg);
});
if (drawable != null) {
mThemeInfo = drawable.mAnimationInfo;
}
} catch (Exception e) {
Log.e(TAG, "Error loading themed clock", e);
}
}
@Override
public Drawable getMonochrome() {
if (mThemeInfo == null) {
return null;
}
Drawable d = mThemeInfo.baseDrawableState.newDrawable().mutate();
if (d instanceof AdaptiveIconDrawable) {
Drawable mono = ((AdaptiveIconDrawable) d).getForeground();
mThemeInfo.applyTime(Calendar.getInstance(), (LayerDrawable) mono);
return mono;
}
return null;
}
/**
* Loads and returns the wrapper from the provided package, or returns null
* if it is unable to load.
*/
public static ClockDrawableWrapper forPackage(Context context, String pkg, int iconDpi,
@Nullable ThemeData themeData) {
try {
PackageManager pm = context.getPackageManager();
ApplicationInfo appInfo = pm.getApplicationInfo(pkg,
PackageManager.MATCH_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA);
Resources res = pm.getResourcesForApplication(appInfo);
ClockDrawableWrapper wrapper = forExtras(appInfo.metaData,
resId -> res.getDrawableForDensity(resId, iconDpi));
if (wrapper != null && themeData != null) {
wrapper.applyThemeData(themeData);
}
return wrapper;
} catch (Exception e) {
Log.d(TAG, "Unable to load clock drawable info", e);
}
return null;
}
@TargetApi(Build.VERSION_CODES.TIRAMISU)
private static ClockDrawableWrapper forExtras(
Bundle metadata, IntFunction<Drawable> drawableProvider) {
if (metadata == null) {
return null;
}
int drawableId = metadata.getInt(ROUND_ICON_METADATA_KEY, 0);
if (drawableId == 0) {
return null;
}
Drawable drawable = drawableProvider.apply(drawableId).mutate();
if (!(drawable instanceof AdaptiveIconDrawable)) {
return null;
}
AdaptiveIconDrawable aid = (AdaptiveIconDrawable) drawable;
ClockDrawableWrapper wrapper = new ClockDrawableWrapper(aid);
AnimationInfo info = wrapper.mAnimationInfo;
info.baseDrawableState = drawable.getConstantState();
info.hourLayerIndex = metadata.getInt(HOUR_INDEX_METADATA_KEY, INVALID_VALUE);
info.minuteLayerIndex = metadata.getInt(MINUTE_INDEX_METADATA_KEY, INVALID_VALUE);
info.secondLayerIndex = metadata.getInt(SECOND_INDEX_METADATA_KEY, INVALID_VALUE);
info.defaultHour = metadata.getInt(DEFAULT_HOUR_METADATA_KEY, 0);
info.defaultMinute = metadata.getInt(DEFAULT_MINUTE_METADATA_KEY, 0);
info.defaultSecond = metadata.getInt(DEFAULT_SECOND_METADATA_KEY, 0);
LayerDrawable foreground = (LayerDrawable) wrapper.getForeground();
int layerCount = foreground.getNumberOfLayers();
if (info.hourLayerIndex < 0 || info.hourLayerIndex >= layerCount) {
info.hourLayerIndex = INVALID_VALUE;
}
if (info.minuteLayerIndex < 0 || info.minuteLayerIndex >= layerCount) {
info.minuteLayerIndex = INVALID_VALUE;
}
if (info.secondLayerIndex < 0 || info.secondLayerIndex >= layerCount) {
info.secondLayerIndex = INVALID_VALUE;
} else if (DISABLE_SECONDS) {
foreground.setDrawable(info.secondLayerIndex, null);
info.secondLayerIndex = INVALID_VALUE;
}
if (ATLEAST_T && aid.getMonochrome() instanceof LayerDrawable) {
wrapper.mThemeInfo = info.copyForIcon(new AdaptiveIconDrawable(
new ColorDrawable(Color.WHITE), aid.getMonochrome().mutate()));
}
info.applyTime(Calendar.getInstance(), foreground);
return wrapper;
}
@Override
public ClockBitmapInfo getExtendedInfo(Bitmap bitmap, int color,
BaseIconFactory iconFactory, float normalizationScale) {
AdaptiveIconDrawable background = new AdaptiveIconDrawable(
getBackground().getConstantState().newDrawable(), null);
Bitmap flattenBG = iconFactory.createScaledBitmap(background,
BaseIconFactory.MODE_HARDWARE_WITH_SHADOW);
// Only pass theme info if mono-icon is enabled
AnimationInfo themeInfo = iconFactory.mMonoIconEnabled ? mThemeInfo : null;
Bitmap themeBG = themeInfo == null ? null : iconFactory.getWhiteShadowLayer();
return new ClockBitmapInfo(bitmap, color, normalizationScale,
mAnimationInfo, flattenBG, themeInfo, themeBG);
}
@Override
public void drawForPersistence(Canvas canvas) {
LayerDrawable foreground = (LayerDrawable) getForeground();
resetLevel(foreground, mAnimationInfo.hourLayerIndex);
resetLevel(foreground, mAnimationInfo.minuteLayerIndex);
resetLevel(foreground, mAnimationInfo.secondLayerIndex);
draw(canvas);
mAnimationInfo.applyTime(Calendar.getInstance(), (LayerDrawable) getForeground());
}
private void resetLevel(LayerDrawable drawable, int index) {
if (index != INVALID_VALUE) {
drawable.getDrawable(index).setLevel(0);
}
}
private static class AnimationInfo {
public ConstantState baseDrawableState;
public int hourLayerIndex;
public int minuteLayerIndex;
public int secondLayerIndex;
public int defaultHour;
public int defaultMinute;
public int defaultSecond;
public AnimationInfo copyForIcon(Drawable icon) {
AnimationInfo result = new AnimationInfo();
result.baseDrawableState = icon.getConstantState();
result.defaultHour = defaultHour;
result.defaultMinute = defaultMinute;
result.defaultSecond = defaultSecond;
result.hourLayerIndex = hourLayerIndex;
result.minuteLayerIndex = minuteLayerIndex;
result.secondLayerIndex = secondLayerIndex;
return result;
}
boolean applyTime(Calendar time, LayerDrawable foregroundDrawable) {
time.setTimeInMillis(System.currentTimeMillis());
// We need to rotate by the difference from the default time if one is specified.
int convertedHour = (time.get(Calendar.HOUR) + (12 - defaultHour)) % 12;
int convertedMinute = (time.get(Calendar.MINUTE) + (60 - defaultMinute)) % 60;
int convertedSecond = (time.get(Calendar.SECOND) + (60 - defaultSecond)) % 60;
boolean invalidate = false;
if (hourLayerIndex != INVALID_VALUE) {
final Drawable hour = foregroundDrawable.getDrawable(hourLayerIndex);
if (hour.setLevel(convertedHour * 60 + time.get(Calendar.MINUTE))) {
invalidate = true;
}
}
if (minuteLayerIndex != INVALID_VALUE) {
final Drawable minute = foregroundDrawable.getDrawable(minuteLayerIndex);
if (minute.setLevel(time.get(Calendar.HOUR) * 60 + convertedMinute)) {
invalidate = true;
}
}
if (secondLayerIndex != INVALID_VALUE) {
final Drawable second = foregroundDrawable.getDrawable(secondLayerIndex);
if (second.setLevel(convertedSecond * LEVELS_PER_SECOND)) {
invalidate = true;
}
}
return invalidate;
}
}
static class ClockBitmapInfo extends BitmapInfo {
public final float boundsOffset;
public final AnimationInfo animInfo;
public final Bitmap mFlattenedBackground;
public final AnimationInfo themeData;
public final Bitmap themeBackground;
ClockBitmapInfo(Bitmap icon, int color, float scale,
AnimationInfo animInfo, Bitmap background,
AnimationInfo themeInfo, Bitmap themeBackground) {
super(icon, color);
this.boundsOffset = Math.max(ShadowGenerator.BLUR_FACTOR, (1 - scale) / 2);
this.animInfo = animInfo;
this.mFlattenedBackground = background;
this.themeData = themeInfo;
this.themeBackground = themeBackground;
}
@Override
@TargetApi(Build.VERSION_CODES.TIRAMISU)
public FastBitmapDrawable newIcon(Context context,
@DrawableCreationFlags int creationFlags) {
AnimationInfo info;
Bitmap bg;
int themedFgColor;
ColorFilter bgFilter;
if ((creationFlags & FLAG_THEMED) != 0 && themeData != null) {
int[] colors = ThemedIconDrawable.getColors(context);
Drawable tintedDrawable = themeData.baseDrawableState.newDrawable().mutate();
themedFgColor = colors[1];
tintedDrawable.setTint(colors[1]);
info = themeData.copyForIcon(tintedDrawable);
bg = themeBackground;
bgFilter = new BlendModeColorFilter(colors[0], BlendMode.SRC_IN);
} else {
info = animInfo;
themedFgColor = NO_COLOR;
bg = mFlattenedBackground;
bgFilter = null;
}
if (info == null) {
return super.newIcon(context, creationFlags);
}
ClockIconDrawable.ClockConstantState cs = new ClockIconDrawable.ClockConstantState(
icon, color, themedFgColor, boundsOffset, info, bg, bgFilter);
FastBitmapDrawable d = cs.newDrawable();
applyFlags(context, d, creationFlags);
return d;
}
@Override
public boolean canPersist() {
return false;
}
@Override
public BitmapInfo clone() {
return copyInternalsTo(new ClockBitmapInfo(icon, color, 1 - 2 * boundsOffset, animInfo,
mFlattenedBackground, themeData, themeBackground));
}
}
private static class ClockIconDrawable extends FastBitmapDrawable implements Runnable {
private final Calendar mTime = Calendar.getInstance();
private final float mBoundsOffset;
private final AnimationInfo mAnimInfo;
private final Bitmap mBG;
private final Paint mBgPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
private final ColorFilter mBgFilter;
private final int mThemedFgColor;
private final AdaptiveIconDrawable mFullDrawable;
private final LayerDrawable mFG;
private final float mCanvasScale;
ClockIconDrawable(ClockConstantState cs) {
super(cs.mBitmap, cs.mIconColor);
mBoundsOffset = cs.mBoundsOffset;
mAnimInfo = cs.mAnimInfo;
mBG = cs.mBG;
mBgFilter = cs.mBgFilter;
mBgPaint.setColorFilter(cs.mBgFilter);
mThemedFgColor = cs.mThemedFgColor;
mFullDrawable =
(AdaptiveIconDrawable) mAnimInfo.baseDrawableState.newDrawable().mutate();
mFG = (LayerDrawable) mFullDrawable.getForeground();
// Time needs to be applied here since drawInternal is NOT guaranteed to be called
// before this foreground drawable is shown on the screen.
mAnimInfo.applyTime(mTime, mFG);
mCanvasScale = 1 - 2 * mBoundsOffset;
}
@Override
public void setAlpha(int alpha) {
super.setAlpha(alpha);
mBgPaint.setAlpha(alpha);
mFG.setAlpha(alpha);
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
// b/211896569 AdaptiveIcon does not work properly when bounds
// are not aligned to top/left corner
mFullDrawable.setBounds(0, 0, bounds.width(), bounds.height());
}
@Override
public void drawInternal(Canvas canvas, Rect bounds) {
if (mAnimInfo == null) {
super.drawInternal(canvas, bounds);
return;
}
canvas.drawBitmap(mBG, null, bounds, mBgPaint);
// prepare and draw the foreground
mAnimInfo.applyTime(mTime, mFG);
int saveCount = canvas.save();
canvas.translate(bounds.left, bounds.top);
canvas.scale(mCanvasScale, mCanvasScale, bounds.width() / 2, bounds.height() / 2);
canvas.clipPath(mFullDrawable.getIconMask());
mFG.draw(canvas);
canvas.restoreToCount(saveCount);
reschedule();
}
@Override
public boolean isThemed() {
return mBgPaint.getColorFilter() != null;
}
@Override
protected void updateFilter() {
super.updateFilter();
int alpha = mIsDisabled ? (int) (mDisabledAlpha * FULLY_OPAQUE) : FULLY_OPAQUE;
setAlpha(alpha);
mBgPaint.setColorFilter(mIsDisabled ? getDisabledColorFilter() : mBgFilter);
mFG.setColorFilter(mIsDisabled ? getDisabledColorFilter() : null);
}
@Override
public int getIconColor() {
return isThemed() ? mThemedFgColor : super.getIconColor();
}
@Override
public void run() {
if (sRunningInTest) {
Log.d("b/319168409", "running this: " + this);
}
if (mAnimInfo.applyTime(mTime, mFG)) {
invalidateSelf();
} else {
reschedule();
}
}
@Override
public boolean setVisible(boolean visible, boolean restart) {
boolean result = super.setVisible(visible, restart);
if (visible) {
reschedule();
} else {
if (sRunningInTest) {
Log.d("b/319168409", "unScheduling self invisible this: " + this);
}
unscheduleSelf(this);
}
return result;
}
private void reschedule() {
if (!isVisible()) {
return;
}
if (sRunningInTest) {
Log.d("b/319168409", "unScheduling self this: " + this);
}
unscheduleSelf(this);
final long upTime = SystemClock.uptimeMillis();
final long step = TICK_MS; /* tick every 200 ms */
if (sRunningInTest) {
Log.d("b/319168409", "scheduling self this: " + this, new Throwable());
}
scheduleSelf(this, upTime - ((upTime % step)) + step);
}
@Override
public FastBitmapConstantState newConstantState() {
return new ClockConstantState(mBitmap, mIconColor, mThemedFgColor, mBoundsOffset,
mAnimInfo, mBG, mBgPaint.getColorFilter());
}
private static class ClockConstantState extends FastBitmapConstantState {
private final float mBoundsOffset;
private final AnimationInfo mAnimInfo;
private final Bitmap mBG;
private final ColorFilter mBgFilter;
private final int mThemedFgColor;
ClockConstantState(Bitmap bitmap, int color, int themedFgColor,
float boundsOffset, AnimationInfo animInfo, Bitmap bg, ColorFilter bgFilter) {
super(bitmap, color);
mBoundsOffset = boundsOffset;
mAnimInfo = animInfo;
mBG = bg;
mBgFilter = bgFilter;
mThemedFgColor = themedFgColor;
}
@Override
public FastBitmapDrawable createDrawable() {
return new ClockIconDrawable(this);
}
}
}
}

View File

@@ -0,0 +1,138 @@
/*
* 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 androidx.annotation.NonNull;
import java.util.Arrays;
/**
* Utility class for extracting colors from a bitmap.
*/
public class ColorExtractor {
private final int NUM_SAMPLES = 20;
@NonNull
private final float[] mTmpHsv = new float[3];
@NonNull
private final float[] mTmpHueScoreHistogram = new float[360];
@NonNull
private final int[] mTmpPixels = new int[NUM_SAMPLES];
@NonNull
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(@NonNull final 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
*/
protected int findDominantColorByHue(@NonNull final Bitmap bitmap, final 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;
}
}

View File

@@ -0,0 +1,150 @@
/*
* 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;
private static final int MIN_DOT_SIZE = 1;
public DotRenderer(int iconSizePx, Path iconShapePath, int pathSize) {
int size = Math.round(SIZE_PERCENTAGE * iconSizePx);
if (size <= 0) {
size = MIN_DOT_SIZE;
}
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.dotColor);
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 dotColor;
/** The color (possibly based on the icon) to use for a predicted app. */
@ViewDebug.ExportedProperty(category = "notification dot", formatToHexString = true)
public int appColor;
/** 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;
}
}

View File

@@ -0,0 +1,453 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.icons;
import static com.android.launcher3.icons.BaseIconFactory.getBadgeSizeForIconSize;
import static com.android.launcher3.icons.BitmapInfo.FLAG_NO_BADGE;
import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED;
import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
import android.animation.ObjectAnimator;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.FloatProperty;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.graphics.ColorUtils;
import com.android.launcher3.icons.BitmapInfo.DrawableCreationFlags;
public class FastBitmapDrawable extends Drawable implements Drawable.Callback {
private static final Interpolator ACCEL = new AccelerateInterpolator();
private static final Interpolator DEACCEL = new DecelerateInterpolator();
private static final Interpolator HOVER_EMPHASIZED_DECELERATE_INTERPOLATOR =
new PathInterpolator(0.05f, 0.7f, 0.1f, 1.0f);
@VisibleForTesting protected static final float PRESSED_SCALE = 1.1f;
@VisibleForTesting protected static final float HOVERED_SCALE = 1.1f;
public static final int WHITE_SCRIM_ALPHA = 138;
private static final float DISABLED_DESATURATION = 1f;
private static final float DISABLED_BRIGHTNESS = 0.5f;
protected static final int FULLY_OPAQUE = 255;
public static final int CLICK_FEEDBACK_DURATION = 200;
public static final int HOVER_FEEDBACK_DURATION = 300;
private static boolean sFlagHoverEnabled = false;
protected final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
protected final Bitmap mBitmap;
protected final int mIconColor;
@Nullable private ColorFilter mColorFilter;
@VisibleForTesting protected boolean mIsPressed;
@VisibleForTesting protected boolean mIsHovered;
protected boolean mIsDisabled;
float mDisabledAlpha = 1f;
@DrawableCreationFlags int mCreationFlags = 0;
// Animator and properties for the fast bitmap drawable's scale
@VisibleForTesting protected static final FloatProperty<FastBitmapDrawable> SCALE
= new FloatProperty<FastBitmapDrawable>("scale") {
@Override
public Float get(FastBitmapDrawable fastBitmapDrawable) {
return fastBitmapDrawable.mScale;
}
@Override
public void setValue(FastBitmapDrawable fastBitmapDrawable, float value) {
fastBitmapDrawable.mScale = value;
fastBitmapDrawable.invalidateSelf();
}
};
@VisibleForTesting protected ObjectAnimator mScaleAnimation;
private float mScale = 1;
private int mAlpha = 255;
private Drawable mBadge;
public FastBitmapDrawable(Bitmap b) {
this(b, Color.TRANSPARENT);
}
public FastBitmapDrawable(BitmapInfo info) {
this(info.icon, info.color);
}
protected FastBitmapDrawable(Bitmap b, int iconColor) {
mBitmap = b;
mIconColor = iconColor;
setFilterBitmap(true);
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
updateBadgeBounds(bounds);
}
private void updateBadgeBounds(Rect bounds) {
if (mBadge != null) {
setBadgeBounds(mBadge, bounds);
}
}
@Override
public final void draw(Canvas canvas) {
if (mScale != 1f) {
int count = canvas.save();
Rect bounds = getBounds();
canvas.scale(mScale, mScale, bounds.exactCenterX(), bounds.exactCenterY());
drawInternal(canvas, bounds);
if (mBadge != null) {
mBadge.draw(canvas);
}
canvas.restoreToCount(count);
} else {
drawInternal(canvas, getBounds());
if (mBadge != null) {
mBadge.draw(canvas);
}
}
}
protected void drawInternal(Canvas canvas, Rect bounds) {
canvas.drawBitmap(mBitmap, null, bounds, mPaint);
}
/**
* Returns the primary icon color, slightly tinted white
*/
public int getIconColor() {
int whiteScrim = setColorAlphaBound(Color.WHITE, WHITE_SCRIM_ALPHA);
return ColorUtils.compositeColors(whiteScrim, mIconColor);
}
/**
* Returns if this represents a themed icon
*/
public boolean isThemed() {
return false;
}
/**
* Returns true if the drawable was created with theme, even if it doesn't
* support theming itself.
*/
public boolean isCreatedForTheme() {
return isThemed() || (mCreationFlags & FLAG_THEMED) != 0;
}
@Override
public void setColorFilter(ColorFilter cf) {
mColorFilter = cf;
updateFilter();
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
@Override
public void setAlpha(int alpha) {
if (mAlpha != alpha) {
mAlpha = alpha;
mPaint.setAlpha(alpha);
invalidateSelf();
if (mBadge != null) {
mBadge.setAlpha(alpha);
}
}
}
@Override
public void setFilterBitmap(boolean filterBitmap) {
mPaint.setFilterBitmap(filterBitmap);
mPaint.setAntiAlias(filterBitmap);
}
@Override
public int getAlpha() {
return mAlpha;
}
public void resetScale() {
if (mScaleAnimation != null) {
mScaleAnimation.cancel();
mScaleAnimation = null;
}
mScale = 1;
invalidateSelf();
}
public float getAnimatedScale() {
return mScaleAnimation == null ? 1 : mScale;
}
@Override
public int getIntrinsicWidth() {
return mBitmap.getWidth();
}
@Override
public int getIntrinsicHeight() {
return mBitmap.getHeight();
}
@Override
public int getMinimumWidth() {
return getBounds().width();
}
@Override
public int getMinimumHeight() {
return getBounds().height();
}
@Override
public boolean isStateful() {
return true;
}
@Override
public ColorFilter getColorFilter() {
return mPaint.getColorFilter();
}
@Override
protected boolean onStateChange(int[] state) {
boolean isPressed = false;
boolean isHovered = false;
for (int s : state) {
if (s == android.R.attr.state_pressed) {
isPressed = true;
break;
} else if (sFlagHoverEnabled && s == android.R.attr.state_hovered) {
isHovered = true;
// Do not break on hovered state, as pressed state should take precedence.
}
}
if (mIsPressed != isPressed || mIsHovered != isHovered) {
if (mScaleAnimation != null) {
mScaleAnimation.cancel();
}
float endScale = isPressed ? PRESSED_SCALE : (isHovered ? HOVERED_SCALE : 1f);
if (mScale != endScale) {
if (isVisible()) {
Interpolator interpolator =
isPressed != mIsPressed ? (isPressed ? ACCEL : DEACCEL)
: HOVER_EMPHASIZED_DECELERATE_INTERPOLATOR;
int duration =
isPressed != mIsPressed ? CLICK_FEEDBACK_DURATION
: HOVER_FEEDBACK_DURATION;
mScaleAnimation = ObjectAnimator.ofFloat(this, SCALE, endScale);
mScaleAnimation.setDuration(duration);
mScaleAnimation.setInterpolator(interpolator);
mScaleAnimation.start();
} else {
mScale = endScale;
invalidateSelf();
}
}
mIsPressed = isPressed;
mIsHovered = isHovered;
return true;
}
return false;
}
public void setIsDisabled(boolean isDisabled) {
if (mIsDisabled != isDisabled) {
mIsDisabled = isDisabled;
updateFilter();
}
}
protected boolean isDisabled() {
return mIsDisabled;
}
public void setBadge(Drawable badge) {
if (mBadge != null) {
mBadge.setCallback(null);
}
mBadge = badge;
if (mBadge != null) {
mBadge.setCallback(this);
}
updateBadgeBounds(getBounds());
updateFilter();
}
@VisibleForTesting
public Drawable getBadge() {
return mBadge;
}
/**
* Updates the paint to reflect the current brightness and saturation.
*/
protected void updateFilter() {
mPaint.setColorFilter(mIsDisabled ? getDisabledColorFilter(mDisabledAlpha) : mColorFilter);
if (mBadge != null) {
mBadge.setColorFilter(getColorFilter());
}
invalidateSelf();
}
protected FastBitmapConstantState newConstantState() {
return new FastBitmapConstantState(mBitmap, mIconColor);
}
@Override
public final ConstantState getConstantState() {
FastBitmapConstantState cs = newConstantState();
cs.mIsDisabled = mIsDisabled;
if (mBadge != null) {
cs.mBadgeConstantState = mBadge.getConstantState();
}
cs.mCreationFlags = mCreationFlags;
return cs;
}
public static ColorFilter getDisabledColorFilter() {
return getDisabledColorFilter(1);
}
// Returns if the FastBitmapDrawable contains a badge.
public boolean hasBadge() {
return (mCreationFlags & FLAG_NO_BADGE) == 0;
}
private static ColorFilter getDisabledColorFilter(float disabledAlpha) {
ColorMatrix tempBrightnessMatrix = new ColorMatrix();
ColorMatrix tempFilterMatrix = new ColorMatrix();
tempFilterMatrix.setSaturation(1f - DISABLED_DESATURATION);
float scale = 1 - DISABLED_BRIGHTNESS;
int brightnessI = (int) (255 * DISABLED_BRIGHTNESS);
float[] mat = tempBrightnessMatrix.getArray();
mat[0] = scale;
mat[6] = scale;
mat[12] = scale;
mat[4] = brightnessI;
mat[9] = brightnessI;
mat[14] = brightnessI;
mat[18] = disabledAlpha;
tempFilterMatrix.preConcat(tempBrightnessMatrix);
return new ColorMatrixColorFilter(tempFilterMatrix);
}
protected static final int getDisabledColor(int color) {
int component = (Color.red(color) + Color.green(color) + Color.blue(color)) / 3;
float scale = 1 - DISABLED_BRIGHTNESS;
int brightnessI = (int) (255 * DISABLED_BRIGHTNESS);
component = Math.min(Math.round(scale * component + brightnessI), FULLY_OPAQUE);
return Color.rgb(component, component, component);
}
/**
* Sets the bounds for the badge drawable based on the main icon bounds
*/
public static void setBadgeBounds(Drawable badge, Rect iconBounds) {
int size = getBadgeSizeForIconSize(iconBounds.width());
badge.setBounds(iconBounds.right - size, iconBounds.bottom - size,
iconBounds.right, iconBounds.bottom);
}
@Override
public void invalidateDrawable(Drawable who) {
if (who == mBadge) {
invalidateSelf();
}
}
@Override
public void scheduleDrawable(Drawable who, Runnable what, long when) {
if (who == mBadge) {
scheduleSelf(what, when);
}
}
@Override
public void unscheduleDrawable(Drawable who, Runnable what) {
unscheduleSelf(what);
}
/**
* Sets whether hover state functionality is enabled.
*/
public static void setFlagHoverEnabled(boolean isFlagHoverEnabled) {
sFlagHoverEnabled = isFlagHoverEnabled;
}
protected static class FastBitmapConstantState extends ConstantState {
protected final Bitmap mBitmap;
protected final int mIconColor;
// These are initialized later so that subclasses don't need to
// pass everything in constructor
protected boolean mIsDisabled;
private ConstantState mBadgeConstantState;
@DrawableCreationFlags int mCreationFlags = 0;
public FastBitmapConstantState(Bitmap bitmap, int color) {
mBitmap = bitmap;
mIconColor = color;
}
protected FastBitmapDrawable createDrawable() {
return new FastBitmapDrawable(mBitmap, mIconColor);
}
@Override
public final FastBitmapDrawable newDrawable() {
FastBitmapDrawable drawable = createDrawable();
drawable.setIsDisabled(mIsDisabled);
if (mBadgeConstantState != null) {
drawable.setBadge(mBadgeConstantState.newDrawable());
}
drawable.mCreationFlags = mCreationFlags;
return drawable;
}
@Override
public int getChangingConfigurations() {
return 0;
}
}
}

View File

@@ -0,0 +1,145 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.icons;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.RegionIterator;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.ColorDrawable;
import android.util.Log;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.core.graphics.PathParser;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class GraphicsUtils {
private static final String TAG = "GraphicsUtils";
private static final float MASK_SIZE = 100f;
public static Runnable sOnNewBitmapRunnable = () -> { };
/**
* Set the alpha component of {@code color} to be {@code alpha}. Unlike the support lib version,
* it bounds the alpha in valid range instead of throwing an exception to allow for safer
* interpolation of color animations
*/
@ColorInt
public static int setColorAlphaBound(int color, int alpha) {
if (alpha < 0) {
alpha = 0;
} else if (alpha > 255) {
alpha = 255;
}
return (color & 0x00ffffff) | (alpha << 24);
}
/**
* Compresses the bitmap to a byte array for serialization.
*/
public static byte[] flattenBitmap(Bitmap bitmap) {
ByteArrayOutputStream out = new ByteArrayOutputStream(getExpectedBitmapSize(bitmap));
try {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
out.flush();
out.close();
return out.toByteArray();
} catch (IOException e) {
Log.w(TAG, "Could not write bitmap");
return null;
}
}
/**
* Try go guesstimate how much space the icon will take when serialized to avoid unnecessary
* allocations/copies during the write (4 bytes per pixel).
*/
static int getExpectedBitmapSize(Bitmap bitmap) {
return bitmap.getWidth() * bitmap.getHeight() * 4;
}
public static int getArea(Region r) {
RegionIterator itr = new RegionIterator(r);
int area = 0;
Rect tempRect = new Rect();
while (itr.next(tempRect)) {
area += tempRect.width() * tempRect.height();
}
return area;
}
/**
* Utility method to track new bitmap creation
*/
public static void noteNewBitmapCreated() {
sOnNewBitmapRunnable.run();
}
/**
* Returns the default path to be used by an icon
*/
public static Path getShapePath(@NonNull Context context, int size) {
if (IconProvider.CONFIG_ICON_MASK_RES_ID != Resources.ID_NULL) {
Path path = PathParser.createPathFromPathData(
context.getString(IconProvider.CONFIG_ICON_MASK_RES_ID));
if (path != null) {
if (size != MASK_SIZE) {
Matrix m = new Matrix();
float scale = ((float) size) / MASK_SIZE;
m.setScale(scale, scale);
path.transform(m);
}
return path;
}
}
AdaptiveIconDrawable drawable = new AdaptiveIconDrawable(
new ColorDrawable(Color.BLACK), new ColorDrawable(Color.BLACK));
drawable.setBounds(0, 0, size, size);
return new Path(drawable.getIconMask());
}
/**
* Returns the color associated with the attribute
*/
public static int getAttrColor(Context context, int attr) {
TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
int colorAccent = ta.getColor(0, 0);
ta.recycle();
return colorAccent;
}
/**
* Returns the alpha corresponding to the theme attribute {@param attr}
*/
public static float getFloat(Context context, int attr, float defValue) {
TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
float value = ta.getFloat(0, defValue);
ta.recycle();
return value;
}
}

View File

@@ -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 (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));
}
}

View File

@@ -0,0 +1,351 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.icons;
import static android.content.Intent.ACTION_DATE_CHANGED;
import static android.content.Intent.ACTION_TIMEZONE_CHANGED;
import static android.content.Intent.ACTION_TIME_CHANGED;
import static android.content.res.Resources.ID_NULL;
import static android.graphics.drawable.AdaptiveIconDrawable.getExtraInsetFraction;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.LauncherActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.PatternMatcher;
import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.os.BuildCompat;
import com.android.launcher3.util.SafeCloseable;
import java.util.Calendar;
import java.util.function.Supplier;
/**
* Class to handle icon loading from different packages
*/
public class IconProvider {
private final String ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED";
static final int CONFIG_ICON_MASK_RES_ID = Resources.getSystem().getIdentifier(
"config_icon_mask", "string", "android");
private static final String TAG = "IconProvider";
private static final boolean DEBUG = false;
public static final boolean ATLEAST_T = BuildCompat.isAtLeastT();
private static final String ICON_METADATA_KEY_PREFIX = ".dynamic_icons";
private static final String SYSTEM_STATE_SEPARATOR = " ";
protected final Context mContext;
private final ComponentName mCalendar;
private final ComponentName mClock;
public IconProvider(Context context) {
mContext = context;
mCalendar = parseComponentOrNull(context, R.string.calendar_component_name);
mClock = parseComponentOrNull(context, R.string.clock_component_name);
}
/**
* Adds any modification to the provided systemState for dynamic icons. This system state
* is used by caches to check for icon invalidation.
*/
public String getSystemStateForPackage(String systemState, String packageName) {
if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) {
return systemState + SYSTEM_STATE_SEPARATOR + getDay();
} else {
return systemState;
}
}
/**
* Loads the icon for the provided LauncherActivityInfo
*/
public Drawable getIcon(LauncherActivityInfo info, int iconDpi) {
return getIconWithOverrides(info.getApplicationInfo().packageName, iconDpi,
() -> info.getIcon(iconDpi));
}
/**
* Loads the icon for the provided activity info
*/
public Drawable getIcon(ActivityInfo info) {
return getIcon(info, mContext.getResources().getConfiguration().densityDpi);
}
/**
* Loads the icon for the provided activity info
*/
public Drawable getIcon(ActivityInfo info, int iconDpi) {
return getIconWithOverrides(info.applicationInfo.packageName, iconDpi,
() -> loadActivityInfoIcon(info, iconDpi));
}
@TargetApi(Build.VERSION_CODES.TIRAMISU)
private Drawable getIconWithOverrides(String packageName, int iconDpi,
Supplier<Drawable> fallback) {
ThemeData td = getThemeDataForPackage(packageName);
Drawable icon = null;
if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) {
icon = loadCalendarDrawable(iconDpi, td);
} else if (mClock != null && mClock.getPackageName().equals(packageName)) {
icon = ClockDrawableWrapper.forPackage(mContext, mClock.getPackageName(), iconDpi, td);
}
if (icon == null) {
icon = fallback.get();
if (ATLEAST_T && icon instanceof AdaptiveIconDrawable && td != null) {
AdaptiveIconDrawable aid = (AdaptiveIconDrawable) icon;
if (aid.getMonochrome() == null) {
icon = new AdaptiveIconDrawable(aid.getBackground(),
aid.getForeground(), td.loadPaddedDrawable());
}
}
}
return icon;
}
protected ThemeData getThemeDataForPackage(String packageName) {
return null;
}
private Drawable loadActivityInfoIcon(ActivityInfo ai, int density) {
final int iconRes = ai.getIconResource();
Drawable icon = null;
// Get the preferred density icon from the app's resources
if (density != 0 && iconRes != 0) {
try {
final Resources resources = mContext.getPackageManager()
.getResourcesForApplication(ai.applicationInfo);
icon = resources.getDrawableForDensity(iconRes, density);
} catch (NameNotFoundException | Resources.NotFoundException exc) { }
}
// Get the default density icon
if (icon == null) {
icon = ai.loadIcon(mContext.getPackageManager());
}
return icon;
}
@TargetApi(Build.VERSION_CODES.TIRAMISU)
private Drawable loadCalendarDrawable(int iconDpi, @Nullable ThemeData td) {
PackageManager pm = mContext.getPackageManager();
try {
final Bundle metadata = pm.getActivityInfo(
mCalendar,
PackageManager.GET_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA)
.metaData;
final Resources resources = pm.getResourcesForApplication(mCalendar.getPackageName());
final int id = getDynamicIconId(metadata, resources);
if (id != ID_NULL) {
if (DEBUG) Log.d(TAG, "Got icon #" + id);
Drawable drawable = resources.getDrawableForDensity(id, iconDpi, null /* theme */);
if (ATLEAST_T && drawable instanceof AdaptiveIconDrawable && td != null) {
AdaptiveIconDrawable aid = (AdaptiveIconDrawable) drawable;
if (aid.getMonochrome() != null) {
return drawable;
}
if ("array".equals(td.mResources.getResourceTypeName(td.mResID))) {
TypedArray ta = td.mResources.obtainTypedArray(td.mResID);
int monoId = ta.getResourceId(IconProvider.getDay(), ID_NULL);
ta.recycle();
return monoId == ID_NULL ? drawable
: new AdaptiveIconDrawable(aid.getBackground(), aid.getForeground(),
new ThemeData(td.mResources, monoId).loadPaddedDrawable());
}
}
return drawable;
}
} catch (PackageManager.NameNotFoundException e) {
if (DEBUG) {
Log.d(TAG, "Could not get activityinfo or resources for package: "
+ mCalendar.getPackageName());
}
}
return null;
}
/**
* @param metadata metadata of the default activity of Calendar
* @param resources from the Calendar package
* @return the resource id for today's Calendar icon; 0 if resources cannot be found.
*/
private int getDynamicIconId(Bundle metadata, Resources resources) {
if (metadata == null) {
return ID_NULL;
}
String key = mCalendar.getPackageName() + ICON_METADATA_KEY_PREFIX;
final int arrayId = metadata.getInt(key, ID_NULL);
if (arrayId == ID_NULL) {
return ID_NULL;
}
try {
return resources.obtainTypedArray(arrayId).getResourceId(getDay(), ID_NULL);
} catch (Resources.NotFoundException e) {
if (DEBUG) {
Log.d(TAG, "package defines '" + key + "' but corresponding array not found");
}
return ID_NULL;
}
}
/**
* @return Today's day of the month, zero-indexed.
*/
private static int getDay() {
return Calendar.getInstance().get(Calendar.DAY_OF_MONTH) - 1;
}
private static ComponentName parseComponentOrNull(Context context, int resId) {
String cn = context.getString(resId);
return TextUtils.isEmpty(cn) ? null : ComponentName.unflattenFromString(cn);
}
/**
* Returns a string representation of the current system icon state
*/
public String getSystemIconState() {
return (CONFIG_ICON_MASK_RES_ID == ID_NULL
? "" : mContext.getResources().getString(CONFIG_ICON_MASK_RES_ID));
}
/**
* Registers a callback to listen for various system dependent icon changes.
*/
public SafeCloseable registerIconChangeListener(IconChangeListener listener, Handler handler) {
return new IconChangeReceiver(listener, handler);
}
public static class ThemeData {
final Resources mResources;
final int mResID;
public ThemeData(Resources resources, int resID) {
mResources = resources;
mResID = resID;
}
Drawable loadPaddedDrawable() {
if (!"drawable".equals(mResources.getResourceTypeName(mResID))) {
return null;
}
Drawable d = mResources.getDrawable(mResID).mutate();
d = new InsetDrawable(d, .2f);
float inset = getExtraInsetFraction() / (1 + 2 * getExtraInsetFraction());
Drawable fg = new InsetDrawable(d, inset);
return fg;
}
}
private class IconChangeReceiver extends BroadcastReceiver implements SafeCloseable {
private final IconChangeListener mCallback;
private String mIconState;
IconChangeReceiver(IconChangeListener callback, Handler handler) {
mCallback = callback;
mIconState = getSystemIconState();
IntentFilter packageFilter = new IntentFilter(ACTION_OVERLAY_CHANGED);
packageFilter.addDataScheme("package");
packageFilter.addDataSchemeSpecificPart("android", PatternMatcher.PATTERN_LITERAL);
mContext.registerReceiver(this, packageFilter, null, handler);
if (mCalendar != null || mClock != null) {
final IntentFilter filter = new IntentFilter(ACTION_TIMEZONE_CHANGED);
if (mCalendar != null) {
filter.addAction(Intent.ACTION_TIME_CHANGED);
filter.addAction(ACTION_DATE_CHANGED);
}
mContext.registerReceiver(this, filter, null, handler);
}
}
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case ACTION_TIMEZONE_CHANGED:
if (mClock != null) {
mCallback.onAppIconChanged(mClock.getPackageName(), Process.myUserHandle());
}
// follow through
case ACTION_DATE_CHANGED:
case ACTION_TIME_CHANGED:
if (mCalendar != null) {
for (UserHandle user
: context.getSystemService(UserManager.class).getUserProfiles()) {
mCallback.onAppIconChanged(mCalendar.getPackageName(), user);
}
}
break;
case ACTION_OVERLAY_CHANGED: {
String newState = getSystemIconState();
if (!mIconState.equals(newState)) {
mIconState = newState;
mCallback.onSystemIconStateChanged(mIconState);
}
break;
}
}
}
@Override
public void close() {
mContext.unregisterReceiver(this);
}
}
/**
* Listener for receiving icon changes
*/
public interface IconChangeListener {
/**
* Called when the icon for a particular app changes
*/
void onAppIconChanged(String packageName, UserHandle user);
/**
* Called when the global icon state changed, which can typically affect all icons
*/
void onSystemIconStateChanged(String iconState);
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.icons;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import androidx.core.graphics.ColorUtils;
/**
* Subclass which draws a placeholder icon when the actual icon is not yet loaded
*/
public class PlaceHolderIconDrawable extends FastBitmapDrawable {
// Path in [0, 100] bounds.
private final Path mProgressPath;
public PlaceHolderIconDrawable(BitmapInfo info, Context context) {
super(info);
mProgressPath = GraphicsUtils.getShapePath(context, 100);
mPaint.setColor(ColorUtils.compositeColors(
GraphicsUtils.getAttrColor(context, R.attr.loadingIconColor), info.color));
}
@Override
protected void drawInternal(Canvas canvas, Rect bounds) {
int saveCount = canvas.save();
canvas.translate(bounds.left, bounds.top);
canvas.scale(bounds.width() / 100f, bounds.height() / 100f);
canvas.drawPath(mProgressPath, mPaint);
canvas.restoreToCount(saveCount);
}
/** Updates this placeholder to {@code newIcon} with animation. */
public void animateIconUpdate(Drawable newIcon) {
int placeholderColor = mPaint.getColor();
int originalAlpha = Color.alpha(placeholderColor);
ValueAnimator iconUpdateAnimation = ValueAnimator.ofInt(originalAlpha, 0);
iconUpdateAnimation.setDuration(375);
iconUpdateAnimation.addUpdateListener(valueAnimator -> {
int newAlpha = (int) valueAnimator.getAnimatedValue();
int newColor = ColorUtils.setAlphaComponent(placeholderColor, newAlpha);
newIcon.setColorFilter(new PorterDuffColorFilter(newColor, PorterDuff.Mode.SRC_ATOP));
});
iconUpdateAnimation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
newIcon.setColorFilter(null);
}
});
iconUpdateAnimation.start();
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.icons;
import android.graphics.Canvas;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.DrawableWrapper;
/**
* A drawable which clips rounded corner around a child drawable
*/
public class RoundDrawableWrapper extends DrawableWrapper {
private final RectF mTempRect = new RectF();
private final Path mClipPath = new Path();
private final float mRoundedCornersRadius;
public RoundDrawableWrapper(Drawable dr, float radius) {
super(dr);
mRoundedCornersRadius = radius;
}
@Override
protected void onBoundsChange(Rect bounds) {
mTempRect.set(getBounds());
mClipPath.reset();
mClipPath.addRoundRect(mTempRect, mRoundedCornersRadius,
mRoundedCornersRadius, Path.Direction.CCW);
super.onBoundsChange(bounds);
}
@Override
public final void draw(Canvas canvas) {
int saveCount = canvas.save();
canvas.clipPath(mClipPath);
super.draw(canvas);
canvas.restoreToCount(saveCount);
}
}

View File

@@ -0,0 +1,196 @@
/*
* 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.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
/**
* Utility class to add shadows to bitmaps.
*/
public class ShadowGenerator {
public static final boolean ENABLE_SHADOWS = true;
public static final float BLUR_FACTOR = 1.68f/48;
// Percent of actual icon size
public static final float KEY_SHADOW_DISTANCE = 1f/48;
private static final int KEY_SHADOW_ALPHA = 7;
// Percent of actual icon size
private static final float HALF_DISTANCE = 0.5f;
private static final int AMBIENT_SHADOW_ALPHA = 25;
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 drawShadow(Bitmap icon, Canvas out) {
if (ENABLE_SHADOWS) {
int[] offset = new int[2];
mBlurPaint.setMaskFilter(mDefaultBlurMaskFilter);
Bitmap shadow = icon.extractAlpha(mBlurPaint, offset);
// Draw ambient shadow
mDrawPaint.setAlpha(AMBIENT_SHADOW_ALPHA);
out.drawBitmap(shadow, offset[0], offset[1], mDrawPaint);
// Draw key shadow
mDrawPaint.setAlpha(KEY_SHADOW_ALPHA);
out.drawBitmap(shadow, offset[0], offset[1] + KEY_SHADOW_DISTANCE * mIconSize,
mDrawPaint);
}
}
/** package private **/
void addPathShadow(Path path, Canvas out) {
if (ENABLE_SHADOWS) {
mDrawPaint.setMaskFilter(mDefaultBlurMaskFilter);
// Draw ambient shadow
mDrawPaint.setAlpha(AMBIENT_SHADOW_ALPHA);
out.drawPath(path, mDrawPaint);
// Draw key shadow
int save = out.save();
mDrawPaint.setAlpha(KEY_SHADOW_ALPHA);
out.translate(0, KEY_SHADOW_DISTANCE * mIconSize);
out.drawPath(path, mDrawPaint);
out.restoreToCount(save);
mDrawPaint.setMaskFilter(null);
}
}
/**
* Returns the minimum amount by which an icon with {@param bounds} should be scaled
* so that the shadows do not get clipped.
*/
public static float getScaleForBounds(RectF bounds) {
float scale = 1;
if (ENABLE_SHADOWS) {
// For top, left & right, we need same space.
float minSide = Math.min(Math.min(bounds.left, bounds.right), bounds.top);
if (minSide < BLUR_FACTOR) {
scale = (HALF_DISTANCE - BLUR_FACTOR) / (HALF_DISTANCE - minSide);
}
// We are ignoring KEY_SHADOW_DISTANCE because regular icons ignore this at the moment b/298203449
float bottomSpace = BLUR_FACTOR;
if (bounds.bottom < bottomSpace) {
scale = Math.min(scale,
(HALF_DISTANCE - bottomSpace) / (HALF_DISTANCE - bounds.bottom));
}
}
return scale;
}
public static class Builder {
public final RectF bounds = new RectF();
public final int color;
public int ambientShadowAlpha = AMBIENT_SHADOW_ALPHA;
public float shadowBlur;
public float keyShadowDistance;
public int keyShadowAlpha = KEY_SHADOW_ALPHA;
public float radius;
public Builder(int color) {
this.color = color;
}
public Builder setupBlurForSize(int height) {
if (ENABLE_SHADOWS) {
shadowBlur = height * 1f / 24;
keyShadowDistance = height * 1f / 16;
} else {
shadowBlur = 0;
keyShadowDistance = 0;
}
return this;
}
public Bitmap createPill(int width, int height) {
return createPill(width, height, height / 2f);
}
public Bitmap createPill(int width, int height, float r) {
radius = r;
int centerX = Math.round(width / 2f + shadowBlur);
int centerY = Math.round(radius + shadowBlur + keyShadowDistance);
int center = Math.max(centerX, centerY);
bounds.set(0, 0, width, height);
bounds.offsetTo(center - width / 2f, center - height / 2f);
int size = center * 2;
return BitmapRenderer.createHardwareBitmap(size, size, this::drawShadow);
}
public void drawShadow(Canvas c) {
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
p.setColor(color);
if (ENABLE_SHADOWS) {
// Key shadow
p.setShadowLayer(shadowBlur, 0, keyShadowDistance,
setColorAlphaBound(Color.BLACK, keyShadowAlpha));
c.drawRoundRect(bounds, radius, radius, p);
// Ambient shadow
p.setShadowLayer(shadowBlur, 0, 0,
setColorAlphaBound(Color.BLACK, ambientShadowAlpha));
c.drawRoundRect(bounds, radius, radius, p);
}
if (Color.alpha(color) < 255) {
// Clear any content inside the pill-rect for translucent fill.
p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
p.clearShadowLayer();
p.setColor(Color.BLACK);
c.drawRoundRect(bounds, radius, radius, p);
p.setXfermode(null);
p.setColor(color);
c.drawRoundRect(bounds, radius, radius, p);
}
}
}
}

View File

@@ -0,0 +1,130 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.icons;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BlendMode;
import android.graphics.BlendModeColorFilter;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Rect;
/**
* Class to handle monochrome themed app icons
*/
@SuppressWarnings("NewApi")
public class ThemedIconDrawable extends FastBitmapDrawable {
public static final String TAG = "ThemedIconDrawable";
final BitmapInfo bitmapInfo;
final int colorFg, colorBg;
// The foreground/monochrome icon for the app
private final Bitmap mMonoIcon;
private final Paint mMonoPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
private final Bitmap mBgBitmap;
private final Paint mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
private final ColorFilter mBgFilter, mMonoFilter;
protected ThemedIconDrawable(ThemedConstantState constantState) {
super(constantState.mBitmap, constantState.colorFg);
bitmapInfo = constantState.bitmapInfo;
colorBg = constantState.colorBg;
colorFg = constantState.colorFg;
mMonoIcon = bitmapInfo.mMono;
mMonoFilter = new BlendModeColorFilter(colorFg, BlendMode.SRC_IN);
mMonoPaint.setColorFilter(mMonoFilter);
mBgBitmap = bitmapInfo.mWhiteShadowLayer;
mBgFilter = new BlendModeColorFilter(colorBg, BlendMode.SRC_IN);
mBgPaint.setColorFilter(mBgFilter);
}
@Override
protected void drawInternal(Canvas canvas, Rect bounds) {
canvas.drawBitmap(mBgBitmap, null, bounds, mBgPaint);
canvas.drawBitmap(mMonoIcon, null, bounds, mMonoPaint);
}
@Override
protected void updateFilter() {
super.updateFilter();
int alpha = mIsDisabled ? (int) (mDisabledAlpha * FULLY_OPAQUE) : FULLY_OPAQUE;
mBgPaint.setAlpha(alpha);
mBgPaint.setColorFilter(mIsDisabled ? new BlendModeColorFilter(
getDisabledColor(colorBg), BlendMode.SRC_IN) : mBgFilter);
mMonoPaint.setAlpha(alpha);
mMonoPaint.setColorFilter(mIsDisabled ? new BlendModeColorFilter(
getDisabledColor(colorFg), BlendMode.SRC_IN) : mMonoFilter);
}
@Override
public boolean isThemed() {
return true;
}
@Override
public FastBitmapConstantState newConstantState() {
return new ThemedConstantState(bitmapInfo, colorBg, colorFg);
}
static class ThemedConstantState extends FastBitmapConstantState {
final BitmapInfo bitmapInfo;
final int colorFg, colorBg;
public ThemedConstantState(BitmapInfo bitmapInfo, int colorBg, int colorFg) {
super(bitmapInfo.icon, bitmapInfo.color);
this.bitmapInfo = bitmapInfo;
this.colorBg = colorBg;
this.colorFg = colorFg;
}
@Override
public FastBitmapDrawable createDrawable() {
return new ThemedIconDrawable(this);
}
}
public static FastBitmapDrawable newDrawable(BitmapInfo info, Context context) {
int[] colors = getColors(context);
return new ThemedConstantState(info, colors[0], colors[1]).newDrawable();
}
/**
* Get an int array representing background and foreground colors for themed icons
*/
public static int[] getColors(Context context) {
Resources res = context.getResources();
int[] colors = new int[2];
colors[0] = res.getColor(R.color.themed_icon_background_color);
colors[1] = res.getColor(R.color.themed_icon_color);
return colors;
}
@Override
public int getIconColor() {
return colorFg;
}
}

View File

@@ -0,0 +1,183 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.icons;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.Resources.Theme;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.DrawableWrapper;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.Nullable;
/**
* A drawable used for drawing user badge. It draws a circle around the actual badge,
* and has support for theming.
*/
public class UserBadgeDrawable extends DrawableWrapper {
private static final float VIEWPORT_SIZE = 24;
private static final float CENTER = VIEWPORT_SIZE / 2;
private static final float BG_RADIUS = 11;
private static final float SHADOW_RADIUS = 11.5f;
private static final float SHADOW_OFFSET_Y = 0.25f;
private static final int SHADOW_COLOR = 0x11000000;
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final int mBaseColor;
private final int mBgColor;
private boolean mShouldDrawBackground = true;
@VisibleForTesting
public final boolean mIsThemed;
public UserBadgeDrawable(Context context, int badgeRes, int colorRes, boolean isThemed) {
super(context.getDrawable(badgeRes));
mIsThemed = isThemed;
if (isThemed) {
mutate();
mBaseColor = context.getColor(R.color.themed_badge_icon_color);
mBgColor = context.getColor(R.color.themed_badge_icon_background_color);
} else {
mBaseColor = context.getColor(colorRes);
mBgColor = Color.WHITE;
}
setTint(mBaseColor);
}
private UserBadgeDrawable(Drawable base, int bgColor, int baseColor,
boolean shouldDrawBackground) {
super(base);
mIsThemed = false;
mBgColor = bgColor;
mBaseColor = baseColor;
mShouldDrawBackground = shouldDrawBackground;
}
@Override
public void draw(@NonNull Canvas canvas) {
if (mShouldDrawBackground) {
Rect b = getBounds();
int saveCount = canvas.save();
canvas.translate(b.left, b.top);
canvas.scale(b.width() / VIEWPORT_SIZE, b.height() / VIEWPORT_SIZE);
mPaint.setColor(SHADOW_COLOR);
canvas.drawCircle(CENTER, CENTER + SHADOW_OFFSET_Y, SHADOW_RADIUS, mPaint);
mPaint.setColor(mBgColor);
canvas.drawCircle(CENTER, CENTER, BG_RADIUS, mPaint);
canvas.restoreToCount(saveCount);
}
super.draw(canvas);
}
@Override
public void setColorFilter(@Nullable ColorFilter filter) {
if (filter == null) {
super.setTint(mBaseColor);
} else if (filter instanceof ColorMatrixColorFilter cf) {
ColorMatrix cm = new ColorMatrix();
cf.getColorMatrix(cm);
ColorMatrix cm2 = new ColorMatrix();
float[] base = cm2.getArray();
base[0] = Color.red(mBaseColor) / 255f;
base[6] = Color.green(mBaseColor) / 255f;
base[12] = Color.blue(mBaseColor) / 255f;
base[18] = Color.alpha(mBaseColor) / 255f;
cm2.postConcat(cm);
super.setColorFilter(new ColorMatrixColorFilter(cm2));
} else {
// fail safe
Paint p = new Paint();
p.setColorFilter(filter);
Bitmap b = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
new Canvas(b).drawPaint(p);
super.setTint(b.getPixel(0, 0));
}
}
public void setShouldDrawBackground(boolean shouldDrawBackground) {
mutate();
mShouldDrawBackground = shouldDrawBackground;
}
@Override
public ConstantState getConstantState() {
return new MyConstantState(
getDrawable().getConstantState(), mBgColor, mBaseColor, mShouldDrawBackground);
}
private static class MyConstantState extends ConstantState {
private final ConstantState mBase;
private final int mBgColor;
private final int mBaseColor;
private final boolean mShouldDrawBackground;
MyConstantState(ConstantState base, int bgColor, int baseColor,
boolean shouldDrawBackground) {
mBase = base;
mBgColor = bgColor;
mBaseColor = baseColor;
mShouldDrawBackground = shouldDrawBackground;
}
@Override
public int getChangingConfigurations() {
return mBase.getChangingConfigurations();
}
@Override
@NonNull
public Drawable newDrawable() {
return new UserBadgeDrawable(
mBase.newDrawable(), mBgColor, mBaseColor, mShouldDrawBackground);
}
@Override
@NonNull
public Drawable newDrawable(Resources res) {
return new UserBadgeDrawable(
mBase.newDrawable(res), mBgColor, mBaseColor, mShouldDrawBackground);
}
@Override
@NonNull
public Drawable newDrawable(Resources res, Theme theme) {
return new UserBadgeDrawable(
mBase.newDrawable(res, theme), mBgColor, mBaseColor, mShouldDrawBackground);
}
}
}

View File

@@ -0,0 +1,797 @@
/*
* 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 android.graphics.BitmapFactory.decodeByteArray;
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.flattenBitmap;
import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
import static com.android.launcher3.icons.cache.IconCacheUpdateHandler.ICON_UPDATE_TOKEN;
import static java.util.Objects.requireNonNull;
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.Bitmap.Config;
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.SystemClock;
import android.os.Trace;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import com.android.launcher3.icons.BaseIconFactory;
import com.android.launcher3.icons.BaseIconFactory.IconOptions;
import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.FlagOp;
import com.android.launcher3.util.SQLiteCacheHelper;
import java.nio.ByteBuffer;
import java.util.AbstractMap;
import java.util.Arrays;
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;
// A format string which returns the original string as is.
private static final String IDENTITY_FORMAT_STRING = "%1$s";
// 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;
@NonNull
public CharSequence title = "";
@NonNull
public CharSequence contentDescription = "";
}
@NonNull
protected final Context mContext;
@NonNull
protected final PackageManager mPackageManager;
@NonNull
private final Map<ComponentKey, CacheEntry> mCache;
@NonNull
protected final Handler mWorkerHandler;
protected int mIconDpi;
@NonNull
protected IconDB mIconDb;
@NonNull
protected LocaleList mLocaleList = LocaleList.getEmptyLocaleList();
@NonNull
protected String mSystemState = "";
@Nullable
private BitmapInfo mDefaultIcon;
@NonNull
private final SparseArray<FlagOp> mUserFlagOpMap = new SparseArray<>();
private final SparseArray<String> mUserFormatString = new SparseArray<>();
@Nullable
private final String mDbFileName;
@NonNull
private final Looper mBgLooper;
public BaseIconCache(@NonNull final Context context, @Nullable final String dbFileName,
@NonNull final Looper bgLooper, final int iconDpi, final int iconPixelSize,
final boolean inMemoryCache) {
mContext = context;
mDbFileName = dbFileName;
mPackageManager = context.getPackageManager();
mBgLooper = bgLooper;
mWorkerHandler = new Handler(mBgLooper);
if (inMemoryCache) {
mCache = new HashMap<>(INITIAL_ICON_CACHE_CAPACITY);
} else {
// Use a dummy cache
mCache = new AbstractMap<ComponentKey, CacheEntry>() {
@Override
public Set<Entry<ComponentKey, CacheEntry>> entrySet() {
return Collections.emptySet();
}
@Override
public CacheEntry put(ComponentKey key, CacheEntry value) {
return value;
}
};
}
updateSystemState();
mIconDpi = iconDpi;
mIconDb = new IconDB(context, dbFileName, iconPixelSize);
}
/**
* Returns the persistable serial number for {@param user}. Subclass should implement proper
* caching strategy to avoid making binder call every time.
*/
protected abstract long getSerialNumberForUser(@NonNull final UserHandle user);
/**
* Return true if the given app is an instant app and should be badged appropriately.
*/
protected abstract boolean isInstantApp(@NonNull final ApplicationInfo info);
/**
* Opens and returns an icon factory. The factory is recycled by the caller.
*/
@NonNull
public abstract BaseIconFactory getIconFactory();
public void updateIconParams(final int iconDpi, final int iconPixelSize) {
mWorkerHandler.post(() -> updateIconParamsBg(iconDpi, iconPixelSize));
}
private synchronized void updateIconParamsBg(final int iconDpi, final int iconPixelSize) {
mIconDpi = iconDpi;
mDefaultIcon = null;
mUserFlagOpMap.clear();
mIconDb.clear();
mIconDb.close();
mIconDb = new IconDB(mContext, mDbFileName, iconPixelSize);
mCache.clear();
}
@Nullable
private Drawable getFullResIcon(@Nullable final Resources resources, final int iconId) {
if (resources != null && iconId != 0) {
try {
return resources.getDrawableForDensity(iconId, mIconDpi);
} catch (Resources.NotFoundException e) { }
}
return getFullResDefaultActivityIcon(mIconDpi);
}
@Nullable
public Drawable getFullResIcon(@NonNull final String packageName, final int iconId) {
try {
return getFullResIcon(mPackageManager.getResourcesForApplication(packageName), iconId);
} catch (PackageManager.NameNotFoundException e) { }
return getFullResDefaultActivityIcon(mIconDpi);
}
@Nullable
public Drawable getFullResIcon(@NonNull final ActivityInfo info) {
try {
return getFullResIcon(mPackageManager.getResourcesForApplication(info.applicationInfo),
info.getIconResource());
} catch (PackageManager.NameNotFoundException e) { }
return getFullResDefaultActivityIcon(mIconDpi);
}
/**
* Remove any records for the supplied ComponentName.
*/
public synchronized void remove(@NonNull final ComponentName componentName,
@NonNull final UserHandle user) {
mCache.remove(new ComponentKey(componentName, user));
}
/**
* Remove any records for the supplied package name from memory.
*/
private void removeFromMemCacheLocked(@Nullable final String packageName,
@Nullable final 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(@NonNull final String packageName,
@NonNull final 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)});
}
@NonNull
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;
mUserFormatString.clear();
}
@NonNull
protected String getIconSystemState(@Nullable final String packageName) {
return mSystemState;
}
public CharSequence getUserBadgedLabel(CharSequence label, UserHandle user) {
int key = user.hashCode();
int index = mUserFormatString.indexOfKey(key);
String format;
if (index < 0) {
format = mPackageManager.getUserBadgedLabel(IDENTITY_FORMAT_STRING, user).toString();
if (TextUtils.equals(IDENTITY_FORMAT_STRING, format)) {
format = null;
}
mUserFormatString.put(key, format);
} else {
format = mUserFormatString.valueAt(index);
}
return format == null ? label : String.format(format, label);
}
/**
* 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(@NonNull final T object,
@NonNull final CachingLogic<T> cachingLogic, @NonNull final PackageInfo info,
final long userSerial, final 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;
CharSequence entryTitle = cachingLogic.getLabel(object);
if (TextUtils.isEmpty(entryTitle)) {
if (entryTitle == null) {
Log.wtf(TAG, "No label returned from caching logic instance: " + cachingLogic);
}
entryTitle = componentName.getPackageName();;
}
entry.title = entryTitle;
entry.contentDescription = 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(@NonNull final ContentValues values, @NonNull final ComponentName key,
@NonNull final PackageInfo info, final long userSerial, final 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);
}
@NonNull
public synchronized BitmapInfo getDefaultIcon(@NonNull final UserHandle user) {
if (mDefaultIcon == null) {
try (BaseIconFactory li = getIconFactory()) {
mDefaultIcon = li.makeDefaultIcon();
}
}
return mDefaultIcon.withFlags(getUserFlagOpLocked(user));
}
@NonNull
protected FlagOp getUserFlagOpLocked(@NonNull final UserHandle user) {
int key = user.hashCode();
int index;
if ((index = mUserFlagOpMap.indexOfKey(key)) >= 0) {
return mUserFlagOpMap.valueAt(index);
} else {
try (BaseIconFactory li = getIconFactory()) {
FlagOp op = li.getBitmapFlagOp(new IconOptions().setUser(user));
mUserFlagOpMap.put(key, op);
return op;
}
}
}
public boolean isDefaultIcon(@NonNull final BitmapInfo icon, @NonNull final 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.
*/
@NonNull
protected <T> CacheEntry cacheLocked(
@NonNull final ComponentName componentName, @NonNull final UserHandle user,
@NonNull final Supplier<T> infoProvider, @NonNull final CachingLogic<T> cachingLogic,
final boolean usePackageIcon, final boolean useLowResIcon) {
return cacheLocked(
componentName,
user,
infoProvider,
cachingLogic,
null,
usePackageIcon,
useLowResIcon);
}
@NonNull
protected <T> CacheEntry cacheLocked(
@NonNull final ComponentName componentName, @NonNull final UserHandle user,
@NonNull final Supplier<T> infoProvider, @NonNull final CachingLogic<T> cachingLogic,
@Nullable final Cursor cursor, final boolean usePackageIcon,
final 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;
boolean cacheEntryUpdated = cursor == null
? getEntryFromDBLocked(cacheKey, entry, useLowResIcon)
: updateTitleAndIconLocked(cacheKey, entry, cursor, useLowResIcon);
if (!cacheEntryUpdated) {
object = infoProvider.get();
providerFetchedOnce = true;
loadFallbackIcon(
object,
entry,
cachingLogic,
usePackageIcon,
/* usePackageTitle= */ true,
componentName,
user);
}
if (TextUtils.isEmpty(entry.title)) {
if (object == null && !providerFetchedOnce) {
object = infoProvider.get();
providerFetchedOnce = true;
}
if (object != null) {
loadFallbackTitle(object, entry, cachingLogic, user);
}
}
}
return entry;
}
/**
* Fallback method for loading an icon bitmap.
*/
protected <T> void loadFallbackIcon(@Nullable final T object, @NonNull final CacheEntry entry,
@NonNull final CachingLogic<T> cachingLogic, final boolean usePackageIcon,
final boolean usePackageTitle, @NonNull final ComponentName componentName,
@NonNull final UserHandle user) {
if (object != null) {
entry.bitmap = cachingLogic.loadIcon(mContext, object);
} else {
if (usePackageIcon) {
CacheEntry packageEntry = getEntryForPackageLocked(
componentName.getPackageName(), user, false);
if (DEBUG) Log.d(TAG, "using package default icon for " +
componentName.toShortString());
entry.bitmap = packageEntry.bitmap;
entry.contentDescription = packageEntry.contentDescription;
if (usePackageTitle) {
entry.title = packageEntry.title;
}
}
if (entry.bitmap == null) {
// TODO: entry.bitmap can never be null, so this should not happen at all.
Log.wtf(TAG, "using default icon for " + componentName.toShortString());
entry.bitmap = getDefaultIcon(user);
}
}
}
/**
* Fallback method for loading an app title.
*/
protected <T> void loadFallbackTitle(
@NonNull final T object, @NonNull final CacheEntry entry,
@NonNull final CachingLogic<T> cachingLogic, @NonNull final UserHandle user) {
entry.title = cachingLogic.getLabel(object);
if (TextUtils.isEmpty(entry.title)) {
entry.title = cachingLogic.getComponent(object).getPackageName();
}
entry.contentDescription = getUserBadgedLabel(
cachingLogic.getDescription(object, entry.title), user);
}
public synchronized void clearMemoryCache() {
assertWorkerThread();
mCache.clear();
}
/**
* Returns true if an icon update is in progress
*/
public boolean isIconUpdateInProgress() {
return mWorkerHandler.hasMessages(0, ICON_UPDATE_TOKEN);
}
/**
* 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(@NonNull final String packageName,
@NonNull final UserHandle user, @Nullable final Bitmap icon,
@Nullable final CharSequence title) {
removeFromMemCacheLocked(packageName, user);
ComponentKey cacheKey = getPackageKey(packageName, user);
CacheEntry entry = mCache.get(cacheKey);
// For icon caching, do not go through DB. Just update the in-memory entry.
if (entry == null) {
entry = new CacheEntry();
}
if (!TextUtils.isEmpty(title)) {
entry.title = title;
}
if (icon != null) {
BaseIconFactory li = getIconFactory();
entry.bitmap = li.createShapedIconBitmap(icon, new IconOptions().setUser(user));
li.close();
}
if (!TextUtils.isEmpty(title) && entry.bitmap.icon != null) {
mCache.put(cacheKey, entry);
}
}
@NonNull
private static ComponentKey getPackageKey(@NonNull final String packageName,
@NonNull final 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.
*/
@WorkerThread
@NonNull
@SuppressWarnings("NewApi")
protected CacheEntry getEntryForPackageLocked(@NonNull final String packageName,
@NonNull final UserHandle user, final 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 (!getEntryFromDBLocked(cacheKey, entry, useLowResIcon)) {
try {
long flags = Process.myUserHandle().equals(user) ? 0 :
PackageManager.GET_UNINSTALLED_PACKAGES;
flags |= PackageManager.MATCH_ARCHIVED_PACKAGES;
PackageInfo info = mPackageManager.getPackageInfo(packageName,
PackageManager.PackageInfoFlags.of(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),
new IconOptions().setUser(user).setInstantApp(isInstantApp(appInfo)));
li.close();
entry.title = appInfo.loadLabel(mPackageManager);
entry.contentDescription = 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 getEntryFromDBLocked(@NonNull final ComponentKey cacheKey,
@NonNull final CacheEntry entry, final boolean lowRes) {
Cursor c = null;
Trace.beginSection("loadIconIndividually");
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()) {
return updateTitleAndIconLocked(cacheKey, entry, c, lowRes);
}
} catch (SQLiteException e) {
Log.d(TAG, "Error reading icon cache", e);
} finally {
if (c != null) {
c.close();
}
Trace.endSection();
}
return false;
}
private boolean updateTitleAndIconLocked(
@NonNull final ComponentKey cacheKey, @NonNull final CacheEntry entry,
@NonNull final Cursor c, final boolean lowRes) {
// Set the alpha to be 255, so that we never have a wrong color
entry.bitmap = BitmapInfo.of(LOW_RES_ICON,
setColorAlphaBound(c.getInt(IconDB.INDEX_COLOR), 255));
entry.title = c.getString(IconDB.INDEX_TITLE);
if (entry.title == null) {
entry.title = "";
entry.contentDescription = "";
} else {
entry.contentDescription = getUserBadgedLabel(entry.title, cacheKey.user);
}
if (!lowRes) {
byte[] data = c.getBlob(IconDB.INDEX_ICON);
if (data == null) {
return false;
}
try {
BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
decodeOptions.inPreferredConfig = Config.HARDWARE;
entry.bitmap = BitmapInfo.of(
requireNonNull(decodeByteArray(data, 0, data.length, decodeOptions)),
entry.bitmap.color);
} catch (Exception e) {
return false;
}
// Decode mono bitmap
data = c.getBlob(IconDB.INDEX_MONO_ICON);
Bitmap icon = entry.bitmap.icon;
if (data != null && data.length == icon.getHeight() * icon.getWidth()) {
Bitmap monoBitmap = Bitmap.createBitmap(
icon.getWidth(), icon.getHeight(), Config.ALPHA_8);
monoBitmap.copyPixelsFromBuffer(ByteBuffer.wrap(data));
Bitmap hwMonoBitmap = monoBitmap.copy(Config.HARDWARE, false /*isMutable*/);
if (hwMonoBitmap != null) {
monoBitmap.recycle();
monoBitmap = hwMonoBitmap;
}
try (BaseIconFactory factory = getIconFactory()) {
entry.bitmap.setMonoIcon(monoBitmap, factory);
}
}
}
entry.bitmap.flags = c.getInt(IconDB.INDEX_FLAGS);
entry.bitmap = entry.bitmap.withFlags(getUserFlagOpLocked(cacheKey.user));
return entry.bitmap != null;
}
/**
* 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 = 34;
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_MONO_ICON = "mono_icon";
public static final String COLUMN_FLAGS = "flags";
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_LOW_RES = new String[] {
COLUMN_COMPONENT,
COLUMN_LABEL,
COLUMN_ICON_COLOR,
COLUMN_FLAGS};
public static final String[] COLUMNS_HIGH_RES = Arrays.copyOf(COLUMNS_LOW_RES,
COLUMNS_LOW_RES.length + 2, String[].class);
static {
COLUMNS_HIGH_RES[COLUMNS_LOW_RES.length] = COLUMN_ICON;
COLUMNS_HIGH_RES[COLUMNS_LOW_RES.length + 1] = COLUMN_MONO_ICON;
}
private static final int INDEX_TITLE = Arrays.asList(COLUMNS_LOW_RES).indexOf(COLUMN_LABEL);
private static final int INDEX_COLOR = Arrays.asList(COLUMNS_LOW_RES)
.indexOf(COLUMN_ICON_COLOR);
private static final int INDEX_FLAGS = Arrays.asList(COLUMNS_LOW_RES).indexOf(COLUMN_FLAGS);
private static final int INDEX_ICON = COLUMNS_LOW_RES.length;
private static final int INDEX_MONO_ICON = INDEX_ICON + 1;
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_MONO_ICON + " BLOB, "
+ COLUMN_ICON_COLOR + " INTEGER NOT NULL DEFAULT 0, "
+ COLUMN_FLAGS + " INTEGER NOT NULL DEFAULT 0, "
+ COLUMN_LABEL + " TEXT, "
+ COLUMN_SYSTEM_STATE + " TEXT, "
+ COLUMN_KEYWORDS + " TEXT, "
+ "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") "
+ ");");
}
}
@NonNull
private ContentValues newContentValues(@NonNull final BitmapInfo bitmapInfo,
@NonNull final String label, @NonNull final String packageName,
@Nullable final String keywords) {
ContentValues values = new ContentValues();
if (bitmapInfo.canPersist()) {
values.put(IconDB.COLUMN_ICON, flattenBitmap(bitmapInfo.icon));
// Persist mono bitmap as alpha channel
Bitmap mono = bitmapInfo.getMono();
if (mono != null && mono.getHeight() == bitmapInfo.icon.getHeight()
&& mono.getWidth() == bitmapInfo.icon.getWidth()
&& mono.getConfig() == Config.ALPHA_8) {
byte[] pixels = new byte[mono.getWidth() * mono.getHeight()];
mono.copyPixelsToBuffer(ByteBuffer.wrap(pixels));
values.put(IconDB.COLUMN_MONO_ICON, pixels);
} else {
values.put(IconDB.COLUMN_MONO_ICON, (byte[]) null);
}
} else {
values.put(IconDB.COLUMN_ICON, (byte[]) null);
values.put(IconDB.COLUMN_MONO_ICON, (byte[]) null);
}
values.put(IconDB.COLUMN_ICON_COLOR, bitmapInfo.color);
values.put(IconDB.COLUMN_FLAGS, bitmapInfo.flags);
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());
}
}
}

View File

@@ -0,0 +1,70 @@
/*
* 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> {
@NonNull
ComponentName getComponent(@NonNull final T object);
@NonNull
UserHandle getUser(@NonNull final T object);
@NonNull
CharSequence getLabel(@NonNull final T object);
@NonNull
default CharSequence getDescription(@NonNull final T object,
@NonNull final CharSequence fallback) {
return fallback;
}
@NonNull
BitmapInfo loadIcon(@NonNull final Context context, @NonNull final T object);
/**
* Provides a option list of keywords to associate with this object
*/
@Nullable
default String getKeywords(@NonNull final T object, @NonNull final LocaleList localeList) {
return null;
}
/**
* Returns the timestamp the entry was last updated in cache.
*/
default long getLastUpdatedTime(@Nullable final T object, @NonNull final PackageInfo info) {
return info.lastUpdateTime;
}
/**
* Returns true the object should be added to mem cache; otherwise returns false.
*/
default boolean addToMemCache() {
return true;
}
}

View File

@@ -0,0 +1,314 @@
/*
* 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;
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 == cachingLogic.getLastUpdatedTime(app, info)
&& 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);
}
}

View File

@@ -0,0 +1,84 @@
package com.android.launcher3.util;
/**
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.content.ComponentName;
import android.os.UserHandle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Arrays;
public class ComponentKey {
public final ComponentName componentName;
public final UserHandle user;
private final int mHashCode;
public ComponentKey(ComponentName componentName, UserHandle user) {
if (componentName == null || user == null) {
throw new NullPointerException();
}
this.componentName = componentName;
this.user = user;
mHashCode = Arrays.hashCode(new Object[] {componentName, user});
}
@Override
public int hashCode() {
return mHashCode;
}
@Override
public boolean equals(Object o) {
ComponentKey other = (ComponentKey) o;
return other.componentName.equals(componentName) && other.user.equals(user);
}
/**
* Encodes a component key as a string of the form [flattenedComponentString#userId].
*/
@Override
public String toString() {
return componentName.flattenToString() + "#" + user.hashCode();
}
/**
* Parses and returns ComponentKey objected from string representation
* Returns null if string is not properly formatted
*/
@Nullable
public static ComponentKey fromString(@NonNull String str) {
int sep = str.indexOf('#');
if (sep < 0 || (sep + 1) >= str.length()) {
return null;
}
ComponentName componentName = ComponentName.unflattenFromString(str.substring(0, sep));
if (componentName == null) {
return null;
}
try {
return new ComponentKey(componentName,
UserHandle.getUserHandleForUid(Integer.parseInt(str.substring(sep + 1))));
} catch (NumberFormatException ex) {
return null;
}
}
}

View File

@@ -0,0 +1,48 @@
/**
* Copyright (C) 2022 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;
/**
* Utility interface for representing flag operations
*/
public interface FlagOp {
FlagOp NO_OP = i -> i;
int apply(int flags);
/**
* Returns a new OP which adds the provided flag after applying all previous operations
*/
default FlagOp addFlag(int flag) {
return i -> apply(i) | flag;
}
/**
* Returns a new OP which removes the provided flag after applying all previous operations
*/
default FlagOp removeFlag(int flag) {
return i -> apply(i) & ~flag;
}
/**
* Returns a new OP which adds or removed the provided flag based on {@code enable} after
* applying all previous operations
*/
default FlagOp setFlag(int flag, boolean enable) {
return enable ? addFlag(flag) : removeFlag(flag);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.util;
/**
* Extension of closeable which does not throw an exception
*/
public interface SafeCloseable extends AutoCloseable {
@Override
void close();
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright (C) 2013 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 com.android.launcher3.icons.BitmapInfo.FLAG_CLONE;
import static com.android.launcher3.icons.BitmapInfo.FLAG_PRIVATE;
import static com.android.launcher3.icons.BitmapInfo.FLAG_WORK;
import android.os.UserHandle;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
/**
* Data class which stores various properties of a {@link android.os.UserHandle}
* which affects rendering
*/
public class UserIconInfo {
public static final int TYPE_MAIN = 0;
public static final int TYPE_WORK = 1;
public static final int TYPE_CLONED = 2;
public static final int TYPE_PRIVATE = 3;
@IntDef({TYPE_MAIN, TYPE_WORK, TYPE_CLONED, TYPE_PRIVATE})
public @interface UserType { }
public final UserHandle user;
@UserType
public final int type;
public final long userSerial;
public UserIconInfo(UserHandle user, @UserType int type) {
this(user, type, 0);
}
public UserIconInfo(UserHandle user, @UserType int type, long userSerial) {
this.user = user;
this.type = type;
this.userSerial = userSerial;
}
public boolean isMain() {
return type == TYPE_MAIN;
}
public boolean isWork() {
return type == TYPE_WORK;
}
public boolean isCloned() {
return type == TYPE_CLONED;
}
public boolean isPrivate() {
return type == TYPE_PRIVATE;
}
@NonNull
public FlagOp applyBitmapInfoFlags(@NonNull FlagOp op) {
return op.setFlag(FLAG_WORK, isWork())
.setFlag(FLAG_CLONE, isCloned())
.setFlag(FLAG_PRIVATE, isPrivate());
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,117 @@
/*
* 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 androidx.annotation.NonNull;
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(@NonNull 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(@NonNull ApplicationInfo info) {
return info.isInstantApp();
}
@NonNull
@Override
public BaseIconFactory getIconFactory() {
return IconFactory.obtain(mContext);
}
public static SimpleIconCache getIconCache(Context context) {
synchronized (CACHE_LOCK) {
if (sIconCache != null) {
return sIconCache;
}
boolean inMemoryCache =
context.getResources().getBoolean(R.bool.simple_cache_enable_im_memory);
String dbFileName = context.getString(R.string.cache_db_name);
HandlerThread bgThread = new HandlerThread("simple-icon-cache");
bgThread.start();
sIconCache = new SimpleIconCache(context.getApplicationContext(), dbFileName,
bgThread.getLooper(), context.getResources().getConfiguration().densityDpi,
context.getResources().getDimensionPixelSize(R.dimen.default_icon_bitmap_size),
inMemoryCache);
return sIconCache;
}
}
}