1 /*
2  * Copyright (C) 2014 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 android.content.pm;
18 
19 import android.annotation.FloatRange;
20 import android.annotation.NonNull;
21 import android.content.ComponentName;
22 import android.content.Context;
23 import android.content.pm.PackageManager.NameNotFoundException;
24 import android.content.res.Resources;
25 import android.graphics.Paint;
26 import android.graphics.drawable.Drawable;
27 import android.icu.text.UnicodeSet;
28 import android.os.UserHandle;
29 import android.os.UserManager;
30 import android.text.TextUtils;
31 import android.util.DisplayMetrics;
32 
33 import com.android.internal.annotations.VisibleForTesting;
34 
35 import java.util.Objects;
36 
37 /**
38  * A representation of an activity that can belong to this user or a managed
39  * profile associated with this user. It can be used to query the label, icon
40  * and badged icon for the activity.
41  */
42 public class LauncherActivityInfo {
43     private final PackageManager mPm;
44     private final LauncherActivityInfoInternal mInternal;
45 
46     private static final UnicodeSet TRIMMABLE_CHARACTERS =
47             new UnicodeSet("[[:White_Space:][:Default_Ignorable_Code_Point:][:gc=Cc:]]",
48                     /* ignoreWhitespace= */ false).freeze();
49 
50     /**
51      * Create a launchable activity object for a given ResolveInfo and user.
52      *
53      * @param context The context for fetching resources.
54 
55      */
LauncherActivityInfo(Context context, LauncherActivityInfoInternal internal)56     LauncherActivityInfo(Context context, LauncherActivityInfoInternal internal) {
57         mPm = context.getPackageManager();
58         mInternal = internal;
59     }
60 
61     /**
62      * Returns the component name of this activity.
63      *
64      * @return ComponentName of the activity
65      */
getComponentName()66     public ComponentName getComponentName() {
67         return mInternal.getComponentName();
68     }
69 
70     /**
71      * Returns the user handle of the user profile that this activity belongs to. In order to
72      * persist the identity of the profile, do not store the UserHandle. Instead retrieve its
73      * serial number from UserManager. You can convert the serial number back to a UserHandle
74      * for later use.
75      *
76      * @see UserManager#getSerialNumberForUser(UserHandle)
77      * @see UserManager#getUserForSerialNumber(long)
78      *
79      * @return The UserHandle of the profile.
80      */
getUser()81     public UserHandle getUser() {
82         return mInternal.getUser();
83     }
84 
85     /**
86      * Retrieves the label for the activity.
87      *
88      * @return The label for the activity.
89      */
getLabel()90     public CharSequence getLabel() {
91         if (!Flags.lightweightInvisibleLabelDetection()) {
92             // TODO: Go through LauncherAppsService
93             return getActivityInfo().loadLabel(mPm);
94         }
95 
96         CharSequence label = trim(getActivityInfo().loadLabel(mPm));
97         // If the trimmed label is empty, use application's label instead
98         if (TextUtils.isEmpty(label)) {
99             label = trim(getApplicationInfo().loadLabel(mPm));
100             // If the trimmed label is still empty, use package name instead
101             if (TextUtils.isEmpty(label)) {
102                 label = getComponentName().getPackageName();
103             }
104         }
105         // TODO: Go through LauncherAppsService
106         return label;
107     }
108 
109     /**
110      * @return Package loading progress, range between [0, 1].
111      */
getLoadingProgress()112     public @FloatRange(from = 0.0, to = 1.0) float getLoadingProgress() {
113         return mInternal.getIncrementalStatesInfo().getProgress();
114     }
115 
116     /**
117      * Returns the icon for this activity, without any badging for the profile.
118      * @param density The preferred density of the icon, zero for default density. Use
119      * density DPI values from {@link DisplayMetrics}.
120      * @see #getBadgedIcon(int)
121      * @see DisplayMetrics
122      * @return The drawable associated with the activity.
123      */
getIcon(int density)124     public Drawable getIcon(int density) {
125         // TODO: Go through LauncherAppsService
126         final int iconRes = getActivityInfo().getIconResource();
127         Drawable icon = null;
128         // Get the preferred density icon from the app's resources
129         if (density != 0 && iconRes != 0) {
130             try {
131                 final Resources resources = mPm.getResourcesForApplication(
132                         getActivityInfo().applicationInfo);
133                 icon = resources.getDrawableForDensity(iconRes, density);
134             } catch (NameNotFoundException | Resources.NotFoundException exc) {
135             }
136         }
137         // Get the default density icon
138         if (icon == null) {
139             icon = getActivityInfo().loadIcon(mPm);
140         }
141         return icon;
142     }
143 
144     /**
145      * Returns the application flags from the ApplicationInfo of the activity.
146      *
147      * @return Application flags
148      * @hide remove before shipping
149      */
getApplicationFlags()150     public int getApplicationFlags() {
151         return getActivityInfo().flags;
152     }
153 
154     /**
155      * Returns the ActivityInfo of the activity.
156      *
157      * @return Activity Info
158      */
159     @NonNull
getActivityInfo()160     public ActivityInfo getActivityInfo() {
161         return mInternal.getActivityInfo();
162     }
163 
164     /**
165      * Returns the application info for the application this activity belongs to.
166      * @return
167      */
getApplicationInfo()168     public ApplicationInfo getApplicationInfo() {
169         return getActivityInfo().applicationInfo;
170     }
171 
172     /**
173      * Returns the time at which the package was first installed.
174      *
175      * @return The time of installation of the package, in milliseconds.
176      */
getFirstInstallTime()177     public long getFirstInstallTime() {
178         try {
179             // TODO: Go through LauncherAppsService
180             return mPm.getPackageInfo(getActivityInfo().packageName,
181                     PackageManager.MATCH_UNINSTALLED_PACKAGES).firstInstallTime;
182         } catch (NameNotFoundException nnfe) {
183             // Sorry, can't find package
184             return 0;
185         }
186     }
187 
188     /**
189      * Returns the name for the activity from  android:name in the manifest.
190      * @return the name from android:name for the activity.
191      */
getName()192     public String getName() {
193         return getActivityInfo().name;
194     }
195 
196     /**
197      * Returns the activity icon with badging appropriate for the profile.
198      * @param density Optional density for the icon, or 0 to use the default density. Use
199      * {@link DisplayMetrics} for DPI values.
200      * @see DisplayMetrics
201      * @return A badged icon for the activity.
202      */
getBadgedIcon(int density)203     public Drawable getBadgedIcon(int density) {
204         Drawable originalIcon = getIcon(density);
205 
206         return mPm.getUserBadgedIcon(originalIcon, mInternal.getUser());
207     }
208 
209     /**
210      * If the {@code ch} is trimmable, return {@code true}. Otherwise, return
211      * {@code false}. If the count of the code points of {@code ch} doesn't
212      * equal 1, return {@code false}.
213      * <p>
214      * There are two types of the trimmable characters.
215      * 1. The character is one of the Default_Ignorable_Code_Point in
216      * <a href="
217      * https://www.unicode.org/Public/UCD/latest/ucd/DerivedCoreProperties.txt">
218      * DerivedCoreProperties.txt</a>, the White_Space in <a href=
219      * "https://www.unicode.org/Public/UCD/latest/ucd/PropList.txt">PropList.txt
220      * </a> or category Cc.
221      * <p>
222      * 2. The character is not supported in the current system font.
223      * {@link android.graphics.Paint#hasGlyph(String)}
224      * <p>
225      *
226      */
isTrimmable(@onNull Paint paint, @NonNull CharSequence ch)227     private static boolean isTrimmable(@NonNull Paint paint, @NonNull CharSequence ch) {
228         Objects.requireNonNull(paint);
229         Objects.requireNonNull(ch);
230 
231         // if ch is empty or it is not a character (i,e, the count of code
232         // point doesn't equal one), return false
233         if (TextUtils.isEmpty(ch)
234                 || Character.codePointCount(ch, /* beginIndex= */ 0, ch.length()) != 1) {
235             return false;
236         }
237 
238         // Return true for the cases as below:
239         // 1. The character is in the TRIMMABLE_CHARACTERS set
240         // 2. The character is not supported in the system font
241         return TRIMMABLE_CHARACTERS.contains(ch) || !paint.hasGlyph(ch.toString());
242     }
243 
244     /**
245      * If the {@code sequence} has some leading trimmable characters, creates a new copy
246      * and removes the trimmable characters from the copy. Otherwise the given
247      * {@code sequence} is returned as it is. Use {@link #isTrimmable(Paint, CharSequence)}
248      * to determine whether the character is trimmable or not.
249      *
250      * @return the trimmed string or the original string that has no
251      *         leading trimmable characters.
252      * @see    #isTrimmable(Paint, CharSequence)
253      * @see    #trim(CharSequence)
254      * @see    #trimEnd(CharSequence)
255      *
256      * @hide
257      */
258     @VisibleForTesting
259     @NonNull
trimStart(@onNull CharSequence sequence)260     public static CharSequence trimStart(@NonNull CharSequence sequence) {
261         Objects.requireNonNull(sequence);
262 
263         if (TextUtils.isEmpty(sequence)) {
264             return sequence;
265         }
266 
267         final Paint paint = new Paint();
268         int trimCount = 0;
269         final int[] codePoints = sequence.codePoints().toArray();
270         for (int i = 0, length = codePoints.length; i < length; i++) {
271             String ch = new String(new int[]{codePoints[i]}, /* offset= */ 0, /* count= */ 1);
272             if (!isTrimmable(paint, ch)) {
273                 break;
274             }
275             trimCount += ch.length();
276         }
277         if (trimCount == 0) {
278             return sequence;
279         }
280         return sequence.subSequence(trimCount, sequence.length());
281     }
282 
283     /**
284      * If the {@code sequence} has some trailing trimmable characters, creates a new copy
285      * and removes the trimmable characters from the copy. Otherwise the given
286      * {@code sequence} is returned as it is. Use {@link #isTrimmable(Paint, CharSequence)}
287      * to determine whether the character is trimmable or not.
288      *
289      * @return the trimmed sequence or the original sequence that has no
290      *         trailing trimmable characters.
291      * @see    #isTrimmable(Paint, CharSequence)
292      * @see    #trimStart(CharSequence)
293      * @see    #trim(CharSequence)
294      *
295      * @hide
296      */
297     @VisibleForTesting
298     @NonNull
trimEnd(@onNull CharSequence sequence)299     public static CharSequence trimEnd(@NonNull CharSequence sequence) {
300         Objects.requireNonNull(sequence);
301 
302         if (TextUtils.isEmpty(sequence)) {
303             return sequence;
304         }
305 
306         final Paint paint = new Paint();
307         int trimCount = 0;
308         final int[] codePoints = sequence.codePoints().toArray();
309         for (int i = codePoints.length - 1; i >= 0; i--) {
310             String ch = new String(new int[]{codePoints[i]}, /* offset= */ 0, /* count= */ 1);
311             if (!isTrimmable(paint, ch)) {
312                 break;
313             }
314             trimCount += ch.length();
315         }
316 
317         if (trimCount == 0) {
318             return sequence;
319         }
320         return sequence.subSequence(0, sequence.length() - trimCount);
321     }
322 
323     /**
324      * If the {@code sequence} has some leading or trailing trimmable characters, creates
325      * a new copy and removes the trimmable characters from the copy. Otherwise the given
326      * {@code sequence} is returned as it is. Use {@link #isTrimmable(Paint, CharSequence)}
327      * to determine whether the character is trimmable or not.
328      *
329      * @return the trimmed sequence or the original sequence that has no leading or
330      *         trailing trimmable characters.
331      * @see    #isTrimmable(Paint, CharSequence)
332      * @see    #trimStart(CharSequence)
333      * @see    #trimEnd(CharSequence)
334      *
335      * @hide
336      */
337     @VisibleForTesting
338     @NonNull
trim(@onNull CharSequence sequence)339     public static CharSequence trim(@NonNull CharSequence sequence) {
340         Objects.requireNonNull(sequence);
341 
342         if (TextUtils.isEmpty(sequence)) {
343             return sequence;
344         }
345 
346         CharSequence result = trimStart(sequence);
347         if (TextUtils.isEmpty(result)) {
348             return result;
349         }
350 
351         return trimEnd(result);
352     }
353 }
354