blob: 449c0daa54beac952c8c87df680393ff9de505fe [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.icons;
import static android.content.Intent.ACTION_DATE_CHANGED;
import static android.content.Intent.ACTION_TIMEZONE_CHANGED;
import static android.content.Intent.ACTION_TIME_CHANGED;
import static android.content.res.Resources.ID_NULL;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.LauncherActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.PatternMatcher;
import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import com.android.launcher3.icons.ThemedIconDrawable.ThemeData;
import com.android.launcher3.util.SafeCloseable;
import org.xmlpull.v1.XmlPullParser;
import java.util.Calendar;
import java.util.Collections;
import java.util.Map;
import java.util.function.Supplier;
/**
* Class to handle icon loading from different packages
*/
public class IconProvider {
private final String ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED";
private static final int CONFIG_ICON_MASK_RES_ID = Resources.getSystem().getIdentifier(
"config_icon_mask", "string", "android");
private static final String TAG_ICON = "icon";
private static final String ATTR_PACKAGE = "package";
private static final String ATTR_DRAWABLE = "drawable";
private static final String TAG = "IconProvider";
private static final boolean DEBUG = false;
private static final String ICON_METADATA_KEY_PREFIX = ".dynamic_icons";
private static final String SYSTEM_STATE_SEPARATOR = " ";
private static final String THEMED_ICON_MAP_FILE = "grayscale_icon_map";
private static final Map<String, ThemeData> DISABLED_MAP = Collections.emptyMap();
private Map<String, ThemeData> mThemedIconMap;
private final Context mContext;
private final ComponentName mCalendar;
private final ComponentName mClock;
static final int ICON_TYPE_DEFAULT = 0;
static final int ICON_TYPE_CALENDAR = 1;
static final int ICON_TYPE_CLOCK = 2;
public IconProvider(Context context) {
this(context, false);
}
public IconProvider(Context context, boolean supportsIconTheme) {
mContext = context;
mCalendar = parseComponentOrNull(context, R.string.calendar_component_name);
mClock = parseComponentOrNull(context, R.string.clock_component_name);
if (!supportsIconTheme) {
// Initialize an empty map if theming is not supported
mThemedIconMap = DISABLED_MAP;
}
}
/**
* Enables or disables icon theme support
*/
public void setIconThemeSupported(boolean isSupported) {
mThemedIconMap = isSupported ? null : DISABLED_MAP;
}
/**
* Adds any modification to the provided systemState for dynamic icons. This system state
* is used by caches to check for icon invalidation.
*/
public String getSystemStateForPackage(String systemState, String packageName) {
if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) {
return systemState + SYSTEM_STATE_SEPARATOR + getDay();
} else {
return systemState;
}
}
/**
* Loads the icon for the provided LauncherActivityInfo
*/
public Drawable getIcon(LauncherActivityInfo info, int iconDpi) {
return getIconWithOverrides(info.getApplicationInfo().packageName, info.getUser(), iconDpi,
() -> info.getIcon(iconDpi));
}
/**
* Loads the icon for the provided activity info
*/
public Drawable getIcon(ActivityInfo info) {
return getIcon(info, mContext.getResources().getConfiguration().densityDpi);
}
/**
* Loads the icon for the provided activity info
*/
public Drawable getIcon(ActivityInfo info, int iconDpi) {
return getIconWithOverrides(info.applicationInfo.packageName,
UserHandle.getUserHandleForUid(info.applicationInfo.uid),
iconDpi, () -> loadActivityInfoIcon(info, iconDpi));
}
private Drawable getIconWithOverrides(String packageName, UserHandle user, int iconDpi,
Supplier<Drawable> fallback) {
Drawable icon = null;
int iconType = ICON_TYPE_DEFAULT;
if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) {
icon = loadCalendarDrawable(iconDpi);
iconType = ICON_TYPE_CALENDAR;
} else if (mClock != null
&& mClock.getPackageName().equals(packageName)
&& Process.myUserHandle().equals(user)) {
icon = loadClockDrawable(iconDpi);
iconType = ICON_TYPE_CLOCK;
}
if (icon == null) {
icon = fallback.get();
iconType = ICON_TYPE_DEFAULT;
}
ThemeData td = getThemedIconMap().get(packageName);
return td != null ? td.wrapDrawable(icon, iconType) : icon;
}
private Drawable loadActivityInfoIcon(ActivityInfo ai, int density) {
final int iconRes = ai.getIconResource();
Drawable icon = null;
// Get the preferred density icon from the app's resources
if (density != 0 && iconRes != 0) {
try {
final Resources resources = mContext.getPackageManager()
.getResourcesForApplication(ai.applicationInfo);
icon = resources.getDrawableForDensity(iconRes, density);
} catch (NameNotFoundException | Resources.NotFoundException exc) { }
}
// Get the default density icon
if (icon == null) {
icon = ai.loadIcon(mContext.getPackageManager());
}
return icon;
}
private Map<String, ThemeData> getThemedIconMap() {
if (mThemedIconMap != null) {
return mThemedIconMap;
}
ArrayMap<String, ThemeData> map = new ArrayMap<>();
try {
Resources res = mContext.getResources();
int resID = res.getIdentifier(THEMED_ICON_MAP_FILE, "xml", mContext.getPackageName());
if (resID != 0) {
XmlResourceParser parser = res.getXml(resID);
final int depth = parser.getDepth();
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG
&& type != XmlPullParser.END_DOCUMENT);
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
if (TAG_ICON.equals(parser.getName())) {
String pkg = parser.getAttributeValue(null, ATTR_PACKAGE);
int iconId = parser.getAttributeResourceValue(null, ATTR_DRAWABLE, 0);
if (iconId != 0 && !TextUtils.isEmpty(pkg)) {
map.put(pkg, new ThemeData(res, iconId));
}
}
}
}
} catch (Exception e) {
Log.e(TAG, "Unable to parse icon map", e);
}
mThemedIconMap = map;
return mThemedIconMap;
}
private Drawable loadCalendarDrawable(int iconDpi) {
PackageManager pm = mContext.getPackageManager();
try {
final Bundle metadata = pm.getActivityInfo(
mCalendar,
PackageManager.GET_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA)
.metaData;
final Resources resources = pm.getResourcesForApplication(mCalendar.getPackageName());
final int id = getDynamicIconId(metadata, resources);
if (id != ID_NULL) {
if (DEBUG) Log.d(TAG, "Got icon #" + id);
return resources.getDrawableForDensity(id, iconDpi, null /* theme */);
}
} catch (PackageManager.NameNotFoundException e) {
if (DEBUG) {
Log.d(TAG, "Could not get activityinfo or resources for package: "
+ mCalendar.getPackageName());
}
}
return null;
}
private Drawable loadClockDrawable(int iconDpi) {
return ClockDrawableWrapper.forPackage(mContext, mClock.getPackageName(), iconDpi);
}
/**
* @param metadata metadata of the default activity of Calendar
* @param resources from the Calendar package
* @return the resource id for today's Calendar icon; 0 if resources cannot be found.
*/
private int getDynamicIconId(Bundle metadata, Resources resources) {
if (metadata == null) {
return ID_NULL;
}
String key = mCalendar.getPackageName() + ICON_METADATA_KEY_PREFIX;
final int arrayId = metadata.getInt(key, ID_NULL);
if (arrayId == ID_NULL) {
return ID_NULL;
}
try {
return resources.obtainTypedArray(arrayId).getResourceId(getDay(), ID_NULL);
} catch (Resources.NotFoundException e) {
if (DEBUG) {
Log.d(TAG, "package defines '" + key + "' but corresponding array not found");
}
return ID_NULL;
}
}
/**
* @return Today's day of the month, zero-indexed.
*/
static int getDay() {
return Calendar.getInstance().get(Calendar.DAY_OF_MONTH) - 1;
}
private static ComponentName parseComponentOrNull(Context context, int resId) {
String cn = context.getString(resId);
return TextUtils.isEmpty(cn) ? null : ComponentName.unflattenFromString(cn);
}
/**
* Returns a string representation of the current system icon state
*/
public String getSystemIconState() {
return (CONFIG_ICON_MASK_RES_ID == ID_NULL
? "" : mContext.getResources().getString(CONFIG_ICON_MASK_RES_ID))
+ (mThemedIconMap == DISABLED_MAP ? ",no-theme" : ",with-theme");
}
/**
* Registers a callback to listen for various system dependent icon changes.
*/
public SafeCloseable registerIconChangeListener(IconChangeListener listener, Handler handler) {
return new IconChangeReceiver(listener, handler);
}
private class IconChangeReceiver extends BroadcastReceiver implements SafeCloseable {
private final IconChangeListener mCallback;
private String mIconState;
IconChangeReceiver(IconChangeListener callback, Handler handler) {
mCallback = callback;
mIconState = getSystemIconState();
IntentFilter packageFilter = new IntentFilter(ACTION_OVERLAY_CHANGED);
packageFilter.addDataScheme("package");
packageFilter.addDataSchemeSpecificPart("android", PatternMatcher.PATTERN_LITERAL);
mContext.registerReceiver(this, packageFilter, null, handler);
if (mCalendar != null || mClock != null) {
final IntentFilter filter = new IntentFilter(ACTION_TIMEZONE_CHANGED);
if (mCalendar != null) {
filter.addAction(Intent.ACTION_TIME_CHANGED);
filter.addAction(ACTION_DATE_CHANGED);
}
mContext.registerReceiver(this, filter, null, handler);
}
}
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case ACTION_TIMEZONE_CHANGED:
if (mClock != null) {
mCallback.onAppIconChanged(mClock.getPackageName(), Process.myUserHandle());
}
// follow through
case ACTION_DATE_CHANGED:
case ACTION_TIME_CHANGED:
if (mCalendar != null) {
for (UserHandle user
: context.getSystemService(UserManager.class).getUserProfiles()) {
mCallback.onAppIconChanged(mCalendar.getPackageName(), user);
}
}
break;
case ACTION_OVERLAY_CHANGED: {
String newState = getSystemIconState();
if (!mIconState.equals(newState)) {
mIconState = newState;
mCallback.onSystemIconStateChanged(mIconState);
}
break;
}
}
}
@Override
public void close() {
mContext.unregisterReceiver(this);
}
}
/**
* Listener for receiving icon changes
*/
public interface IconChangeListener {
/**
* Called when the icon for a particular app changes
*/
void onAppIconChanged(String packageName, UserHandle user);
/**
* Called when the global icon state changed, which can typically affect all icons
*/
void onSystemIconStateChanged(String iconState);
}
}