blob: b2e554b7b6f6c70d9e27abc5e2006927f70883d5 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.icons;
import static android.content.res.Configuration.UI_MODE_NIGHT_MASK;
import static android.content.res.Configuration.UI_MODE_NIGHT_YES;
import static android.content.res.Resources.ID_NULL;
import static com.android.launcher3.icons.GraphicsUtils.getExpectedBitmapSize;
import static com.android.launcher3.icons.IconProvider.ICON_TYPE_CALENDAR;
import static com.android.launcher3.icons.IconProvider.ICON_TYPE_CLOCK;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.os.Process;
import android.os.UserHandle;
import android.util.Log;
import androidx.annotation.Nullable;
import com.android.launcher3.icons.BitmapInfo.Extender;
import com.android.launcher3.icons.cache.BaseIconCache;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
/**
* Class to handle monochrome themed app icons
*/
@SuppressWarnings("NewApi")
public class ThemedIconDrawable extends FastBitmapDrawable {
public static final String TAG = "ThemedIconDrawable";
final ThemedBitmapInfo bitmapInfo;
final int colorFg, colorBg;
// The foreground/monochrome icon for the app
private final Drawable mMonochromeIcon;
private final AdaptiveIconDrawable mBgWrapper;
private final Rect mBadgeBounds;
protected ThemedIconDrawable(ThemedConstantState constantState) {
super(constantState.mBitmap, constantState.colorFg, constantState.mIsDisabled);
bitmapInfo = constantState.bitmapInfo;
colorBg = constantState.colorBg;
colorFg = constantState.colorFg;
mMonochromeIcon = bitmapInfo.mThemeData.loadMonochromeDrawable(colorFg);
mBgWrapper = new AdaptiveIconDrawable(new ColorDrawable(colorBg), null);
mBadgeBounds = bitmapInfo.mUserBadge == null ? null :
new Rect(0, 0, bitmapInfo.mUserBadge.getWidth(), bitmapInfo.mUserBadge.getHeight());
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
mBgWrapper.setBounds(bounds);
mMonochromeIcon.setBounds(bounds);
}
@Override
protected void drawInternal(Canvas canvas, Rect bounds) {
int count = canvas.save();
canvas.scale(bitmapInfo.mNormalizationScale, bitmapInfo.mNormalizationScale,
bounds.exactCenterX(), bounds.exactCenterY());
mPaint.setColor(colorBg);
canvas.drawPath(mBgWrapper.getIconMask(), mPaint);
mMonochromeIcon.draw(canvas);
canvas.restoreToCount(count);
if (mBadgeBounds != null) {
canvas.drawBitmap(bitmapInfo.mUserBadge, mBadgeBounds, getBounds(), mPaint);
}
}
@Override
public boolean isThemed() {
return true;
}
@Override
public ConstantState getConstantState() {
return new ThemedConstantState(bitmapInfo, colorBg, colorFg, mIsDisabled);
}
static class ThemedConstantState extends FastBitmapConstantState {
final ThemedBitmapInfo bitmapInfo;
final int colorFg, colorBg;
public ThemedConstantState(ThemedBitmapInfo bitmapInfo,
int colorBg, int colorFg, boolean isDisabled) {
super(bitmapInfo.icon, bitmapInfo.color, isDisabled);
this.bitmapInfo = bitmapInfo;
this.colorBg = colorBg;
this.colorFg = colorFg;
}
@Override
public FastBitmapDrawable newDrawable() {
return new ThemedIconDrawable(this);
}
}
public static class ThemedBitmapInfo extends BitmapInfo {
final ThemeData mThemeData;
final float mNormalizationScale;
final Bitmap mUserBadge;
public ThemedBitmapInfo(Bitmap icon, int color, ThemeData themeData,
float normalizationScale, Bitmap userBadge) {
super(icon, color);
mThemeData = themeData;
mNormalizationScale = normalizationScale;
mUserBadge = userBadge;
}
@Override
public FastBitmapDrawable newThemedIcon(Context context) {
int[] colors = getColors(context);
FastBitmapDrawable drawable = new ThemedConstantState(this, colors[0], colors[1], false)
.newDrawable();
drawable.mDisabledAlpha = GraphicsUtils.getFloat(context, R.attr.disabledIconAlpha, 1f);
return drawable;
}
@Nullable
public byte[] toByteArray() {
if (isNullOrLowRes()) {
return null;
}
String resName = mThemeData.mResources.getResourceName(mThemeData.mResID);
ByteArrayOutputStream out = new ByteArrayOutputStream(
getExpectedBitmapSize(icon) + 3 + resName.length());
try {
DataOutputStream dos = new DataOutputStream(out);
dos.writeByte(TYPE_THEMED);
dos.writeFloat(mNormalizationScale);
dos.writeUTF(resName);
icon.compress(Bitmap.CompressFormat.PNG, 100, dos);
dos.flush();
dos.close();
return out.toByteArray();
} catch (IOException e) {
Log.w(TAG, "Could not write bitmap");
return null;
}
}
static ThemedBitmapInfo decode(byte[] data, int color,
BitmapFactory.Options decodeOptions, UserHandle user, BaseIconCache iconCache,
Context context) {
try (DataInputStream dis = new DataInputStream(new ByteArrayInputStream(data))) {
dis.readByte(); // type
float normalizationScale = dis.readFloat();
String resName = dis.readUTF();
int resId = context.getResources()
.getIdentifier(resName, "drawable", context.getPackageName());
if (resId == ID_NULL) {
return null;
}
Bitmap userBadgeBitmap = null;
if (!Process.myUserHandle().equals(user)) {
try (BaseIconFactory iconFactory = iconCache.getIconFactory()) {
userBadgeBitmap = iconFactory.getUserBadgeBitmap(user);
}
}
ThemeData themeData = new ThemeData(context.getResources(), resId);
Bitmap icon = BitmapFactory.decodeStream(dis, null, decodeOptions);
return new ThemedBitmapInfo(icon, color, themeData, normalizationScale,
userBadgeBitmap);
} catch (IOException e) {
return null;
}
}
}
public static class ThemeData {
final Resources mResources;
final int mResID;
public ThemeData(Resources resources, int resID) {
mResources = resources;
mResID = resID;
}
Drawable loadMonochromeDrawable(int accentColor) {
Drawable d = mResources.getDrawable(mResID).mutate();
d.setTint(accentColor);
d = new InsetDrawable(d, .2f);
return d;
}
public Drawable wrapDrawable(Drawable original, int iconType) {
if (!(original instanceof AdaptiveIconDrawable)) {
return original;
}
AdaptiveIconDrawable aid = (AdaptiveIconDrawable) original;
String resourceType = mResources.getResourceTypeName(mResID);
if (iconType == ICON_TYPE_CALENDAR && "array".equals(resourceType)) {
TypedArray ta = mResources.obtainTypedArray(mResID);
int id = ta.getResourceId(IconProvider.getDay(), ID_NULL);
ta.recycle();
return id == ID_NULL ? original
: new ThemedAdaptiveIcon(aid, new ThemeData(mResources, id));
} else if (iconType == ICON_TYPE_CLOCK && "array".equals(resourceType)) {
((ClockDrawableWrapper) original).mThemeData = this;
return original;
} else if ("drawable".equals(resourceType)) {
return new ThemedAdaptiveIcon(aid, this);
} else {
return original;
}
}
}
static class ThemedAdaptiveIcon extends AdaptiveIconDrawable implements Extender {
protected final ThemeData mThemeData;
public ThemedAdaptiveIcon(AdaptiveIconDrawable parent, ThemeData themeData) {
super(parent.getBackground(), parent.getForeground());
mThemeData = themeData;
}
@Override
public BitmapInfo getExtendedInfo(Bitmap bitmap, int color, BaseIconFactory iconFactory,
float normalizationScale, UserHandle user) {
Bitmap userBadge = Process.myUserHandle().equals(user)
? null : iconFactory.getUserBadgeBitmap(user);
return new ThemedBitmapInfo(bitmap, color, mThemeData, normalizationScale, userBadge);
}
@Override
public void drawForPersistence(Canvas canvas) {
draw(canvas);
}
@Override
public Drawable getThemedDrawable(Context context) {
int[] colors = getColors(context);
Drawable bg = new ColorDrawable(colors[0]);
float inset = getExtraInsetFraction() / (1 + 2 * getExtraInsetFraction());
Drawable fg = new InsetDrawable(mThemeData.loadMonochromeDrawable(colors[1]), inset);
return new AdaptiveIconDrawable(bg, fg);
}
}
/**
* Get an int array representing background and foreground colors for themed icons
*/
public static int[] getColors(Context context) {
Resources res = context.getResources();
int[] colors = new int[2];
if ((res.getConfiguration().uiMode & UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES) {
colors[0] = res.getColor(android.R.color.system_neutral1_800);
colors[1] = res.getColor(android.R.color.system_accent1_100);
} else {
colors[0] = res.getColor(android.R.color.system_accent1_100);
colors[1] = res.getColor(android.R.color.system_neutral2_700);
}
return colors;
}
}