1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.launcher3.icons;
18 
19 import android.content.BroadcastReceiver;
20 import android.content.ComponentName;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.IntentFilter;
24 import android.content.pm.ActivityInfo;
25 import android.content.pm.LauncherActivityInfo;
26 import android.content.pm.PackageManager;
27 import android.content.res.Resources;
28 import android.graphics.drawable.Drawable;
29 import android.os.Bundle;
30 import android.os.Handler;
31 import android.os.Process;
32 import android.os.UserHandle;
33 import android.text.TextUtils;
34 import android.util.Log;
35 
36 import com.android.launcher3.R;
37 import com.android.launcher3.icons.BitmapInfo.Extender;
38 import com.android.launcher3.pm.UserCache;
39 import com.android.launcher3.util.ComponentKey;
40 import com.android.launcher3.util.SafeCloseable;
41 
42 import java.util.Calendar;
43 import java.util.function.BiConsumer;
44 import java.util.function.BiFunction;
45 
46 /**
47  * Class to handle icon loading from different packages
48  */
49 public class IconProvider {
50 
51     private static final String TAG = "IconProvider";
52     private static final boolean DEBUG = false;
53 
54     private static final String ICON_METADATA_KEY_PREFIX = ".dynamic_icons";
55 
56     private static final String SYSTEM_STATE_SEPARATOR = " ";
57 
58     // Default value returned if there are problems getting resources.
59     private static final int NO_ID = 0;
60 
61     private static final BiFunction<LauncherActivityInfo, Integer, Drawable> LAI_LOADER =
62             LauncherActivityInfo::getIcon;
63 
64     private static final BiFunction<ActivityInfo, PackageManager, Drawable> AI_LOADER =
65             ActivityInfo::loadUnbadgedIcon;
66 
67 
68     private final Context mContext;
69     private final ComponentName mCalendar;
70     private final ComponentName mClock;
71 
IconProvider(Context context)72     public IconProvider(Context context) {
73         mContext = context;
74         mCalendar = parseComponentOrNull(context, R.string.calendar_component_name);
75         mClock = parseComponentOrNull(context, R.string.clock_component_name);
76     }
77 
78     /**
79      * Adds any modification to the provided systemState for dynamic icons. This system state
80      * is used by caches to check for icon invalidation.
81      */
getSystemStateForPackage(String systemState, String packageName)82     public String getSystemStateForPackage(String systemState, String packageName) {
83         if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) {
84             return systemState + SYSTEM_STATE_SEPARATOR + getDay();
85         } else {
86             return systemState;
87         }
88     }
89 
90     /**
91      * Loads the icon for the provided LauncherActivityInfo such that it can be drawn directly
92      * on the UI
93      */
getIconForUI(LauncherActivityInfo info, int iconDpi)94     public Drawable getIconForUI(LauncherActivityInfo info, int iconDpi) {
95         Drawable icon = getIcon(info, iconDpi);
96         if (icon instanceof BitmapInfo.Extender) {
97             ((Extender) icon).prepareToDrawOnUi();
98         }
99         return icon;
100     }
101 
102     /**
103      * Loads the icon for the provided LauncherActivityInfo
104      */
getIcon(LauncherActivityInfo info, int iconDpi)105     public Drawable getIcon(LauncherActivityInfo info, int iconDpi) {
106         return getIcon(info.getApplicationInfo().packageName, info.getUser(),
107                 info, iconDpi, LAI_LOADER);
108     }
109 
110     /**
111      * Loads the icon for the provided activity info
112      */
getIcon(ActivityInfo info, UserHandle user)113     public Drawable getIcon(ActivityInfo info, UserHandle user) {
114         return getIcon(info.applicationInfo.packageName, user, info, mContext.getPackageManager(),
115                 AI_LOADER);
116     }
117 
getIcon(String packageName, UserHandle user, T obj, P param, BiFunction<T, P, Drawable> loader)118     private <T, P> Drawable getIcon(String packageName, UserHandle user, T obj, P param,
119             BiFunction<T, P, Drawable> loader) {
120         Drawable icon = null;
121         if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) {
122             icon = loadCalendarDrawable(0);
123         } else if (mClock != null
124                 && mClock.getPackageName().equals(packageName)
125                 && Process.myUserHandle().equals(user)) {
126             icon = loadClockDrawable(0);
127         }
128         return icon == null ? loader.apply(obj, param) : icon;
129     }
130 
loadCalendarDrawable(int iconDpi)131     private Drawable loadCalendarDrawable(int iconDpi) {
132         PackageManager pm = mContext.getPackageManager();
133         try {
134             final Bundle metadata = pm.getActivityInfo(
135                     mCalendar,
136                     PackageManager.GET_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA)
137                     .metaData;
138             final Resources resources = pm.getResourcesForApplication(mCalendar.getPackageName());
139             final int id = getDynamicIconId(metadata, resources);
140             if (id != NO_ID) {
141                 if (DEBUG) Log.d(TAG, "Got icon #" + id);
142                 return resources.getDrawableForDensity(id, iconDpi, null /* theme */);
143             }
144         } catch (PackageManager.NameNotFoundException e) {
145             if (DEBUG) {
146                 Log.d(TAG, "Could not get activityinfo or resources for package: "
147                         + mCalendar.getPackageName());
148             }
149         }
150         return null;
151     }
152 
loadClockDrawable(int iconDpi)153     private Drawable loadClockDrawable(int iconDpi) {
154         return ClockDrawableWrapper.forPackage(mContext, mClock.getPackageName(), iconDpi);
155     }
156 
isClockIcon(ComponentKey key)157     protected boolean isClockIcon(ComponentKey key) {
158         return mClock != null && mClock.equals(key.componentName)
159                 && Process.myUserHandle().equals(key.user);
160     }
161 
162     /**
163      * @param metadata metadata of the default activity of Calendar
164      * @param resources from the Calendar package
165      * @return the resource id for today's Calendar icon; 0 if resources cannot be found.
166      */
getDynamicIconId(Bundle metadata, Resources resources)167     private int getDynamicIconId(Bundle metadata, Resources resources) {
168         if (metadata == null) {
169             return NO_ID;
170         }
171         String key = mCalendar.getPackageName() + ICON_METADATA_KEY_PREFIX;
172         final int arrayId = metadata.getInt(key, NO_ID);
173         if (arrayId == NO_ID) {
174             return NO_ID;
175         }
176         try {
177             return resources.obtainTypedArray(arrayId).getResourceId(getDay(), NO_ID);
178         } catch (Resources.NotFoundException e) {
179             if (DEBUG) {
180                 Log.d(TAG, "package defines '" + key + "' but corresponding array not found");
181             }
182             return NO_ID;
183         }
184     }
185 
186     /**
187      * @return Today's day of the month, zero-indexed.
188      */
getDay()189     private int getDay() {
190         return Calendar.getInstance().get(Calendar.DAY_OF_MONTH) - 1;
191     }
192 
193 
194     /**
195      * Registers a callback to listen for calendar icon changes.
196      * The callback receives the packageName for the calendar icon
197      */
registerIconChangeListener(Context context, BiConsumer<String, UserHandle> callback, Handler handler)198     public static SafeCloseable registerIconChangeListener(Context context,
199             BiConsumer<String, UserHandle> callback, Handler handler) {
200         ComponentName calendar = parseComponentOrNull(context, R.string.calendar_component_name);
201         ComponentName clock = parseComponentOrNull(context, R.string.clock_component_name);
202 
203         if (calendar == null && clock == null) {
204             return () -> { };
205         }
206 
207         BroadcastReceiver receiver = new DateTimeChangeReceiver(callback);
208         final IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
209         if (calendar != null) {
210             filter.addAction(Intent.ACTION_TIME_CHANGED);
211             filter.addAction(Intent.ACTION_DATE_CHANGED);
212         }
213         context.registerReceiver(receiver, filter, null, handler);
214 
215         return () -> context.unregisterReceiver(receiver);
216     }
217 
218     private static class DateTimeChangeReceiver extends BroadcastReceiver {
219 
220         private final BiConsumer<String, UserHandle> mCallback;
221 
DateTimeChangeReceiver(BiConsumer<String, UserHandle> callback)222         DateTimeChangeReceiver(BiConsumer<String, UserHandle> callback) {
223             mCallback = callback;
224         }
225 
226         @Override
onReceive(Context context, Intent intent)227         public void onReceive(Context context, Intent intent) {
228             if (Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
229                 ComponentName clock = parseComponentOrNull(context, R.string.clock_component_name);
230                 if (clock != null) {
231                     mCallback.accept(clock.getPackageName(), Process.myUserHandle());
232                 }
233             }
234 
235             ComponentName calendar =
236                     parseComponentOrNull(context, R.string.calendar_component_name);
237             if (calendar != null) {
238                 for (UserHandle user : UserCache.INSTANCE.get(context).getUserProfiles()) {
239                     mCallback.accept(calendar.getPackageName(), user);
240                 }
241             }
242 
243         }
244     }
245 
parseComponentOrNull(Context context, int resId)246     private static ComponentName parseComponentOrNull(Context context, int resId) {
247         String cn = context.getString(resId);
248         return TextUtils.isEmpty(cn) ? null : ComponentName.unflattenFromString(cn);
249 
250     }
251 }
252