ShortcutInfo.java revision 2c0ae91f2d22b2c9a3b506d3a7f60bc31f72c57d
1/*
2 * Copyright (C) 2016 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 */
16package android.content.pm;
17
18import android.annotation.IntDef;
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.content.ComponentName;
22import android.content.ContentResolver;
23import android.content.Context;
24import android.content.Intent;
25import android.graphics.drawable.Icon;
26import android.os.Bundle;
27import android.os.Parcel;
28import android.os.Parcelable;
29import android.os.PersistableBundle;
30import android.os.UserHandle;
31
32import com.android.internal.util.Preconditions;
33
34import java.lang.annotation.Retention;
35import java.lang.annotation.RetentionPolicy;
36
37// TODO Enhance javadoc
38/**
39 *
40 * Represents a shortcut from an application.
41 *
42 * <p>Notes about icons:
43 * <ul>
44 *     <li>If an {@link Icon} is a resource, the system keeps the package name and the resource ID.
45 *     Otherwise, the bitmap is fetched when it's registered to ShortcutManager,
46 *     then shrunk if necessary, and persisted.
47 *     <li>The system disallows byte[] icons, because they can easily go over the binder size limit.
48 * </ul>
49 *
50 * @see {@link ShortcutManager}.
51 */
52public final class ShortcutInfo implements Parcelable {
53    /* @hide */
54    public static final int FLAG_DYNAMIC = 1 << 0;
55
56    /* @hide */
57    public static final int FLAG_PINNED = 1 << 1;
58
59    /* @hide */
60    public static final int FLAG_HAS_ICON_RES = 1 << 2;
61
62    /* @hide */
63    public static final int FLAG_HAS_ICON_FILE = 1 << 3;
64
65    /* @hide */
66    public static final int FLAG_KEY_FIELDS_ONLY = 1 << 4;
67
68    /** @hide */
69    @IntDef(flag = true,
70            value = {
71            FLAG_DYNAMIC,
72            FLAG_PINNED,
73            FLAG_HAS_ICON_RES,
74            FLAG_HAS_ICON_FILE,
75            FLAG_KEY_FIELDS_ONLY,
76    })
77    @Retention(RetentionPolicy.SOURCE)
78    public @interface ShortcutFlags {}
79
80    // Cloning options.
81
82    /* @hide */
83    private static final int CLONE_REMOVE_ICON = 1 << 0;
84
85    /* @hide */
86    private static final int CLONE_REMOVE_INTENT = 1 << 1;
87
88    /* @hide */
89    public static final int CLONE_REMOVE_NON_KEY_INFO = 1 << 2;
90
91    /* @hide */
92    public static final int CLONE_REMOVE_FOR_CREATOR = CLONE_REMOVE_ICON;
93
94    /* @hide */
95    public static final int CLONE_REMOVE_FOR_LAUNCHER = CLONE_REMOVE_ICON | CLONE_REMOVE_INTENT;
96
97    /** @hide */
98    @IntDef(flag = true,
99            value = {
100                    CLONE_REMOVE_ICON,
101                    CLONE_REMOVE_INTENT,
102                    CLONE_REMOVE_NON_KEY_INFO,
103                    CLONE_REMOVE_FOR_CREATOR,
104                    CLONE_REMOVE_FOR_LAUNCHER
105            })
106    @Retention(RetentionPolicy.SOURCE)
107    public @interface CloneFlags {}
108
109    private final String mId;
110
111    @NonNull
112    private final String mPackageName;
113
114    @Nullable
115    private ComponentName mActivityComponent;
116
117    @Nullable
118    private Icon mIcon;
119
120    @NonNull
121    private String mTitle;
122
123    @Nullable
124    private String mText;
125
126    /**
127     * Intent *with extras removed*.
128     */
129    @NonNull
130    private Intent mIntent;
131
132    /**
133     * Extras for the intent.
134     */
135    @NonNull
136    private PersistableBundle mIntentPersistableExtras;
137
138    private int mWeight;
139
140    @Nullable
141    private PersistableBundle mExtras;
142
143    private long mLastChangedTimestamp;
144
145    // Internal use only.
146    @ShortcutFlags
147    private int mFlags;
148
149    // Internal use only.
150    private int mIconResourceId;
151
152    // Internal use only.
153    @Nullable
154    private String mBitmapPath;
155
156    private ShortcutInfo(Builder b) {
157        mId = Preconditions.checkStringNotEmpty(b.mId, "Shortcut ID must be provided");
158
159        // Note we can't do other null checks here because SM.updateShortcuts() takes partial
160        // information.
161        mPackageName = b.mContext.getPackageName();
162        mActivityComponent = b.mActivityComponent;
163        mIcon = b.mIcon;
164        mTitle = b.mTitle;
165        mText = b.mText;
166        mIntent = b.mIntent;
167        if (mIntent != null) {
168            final Bundle intentExtras = mIntent.getExtras();
169            if (intentExtras != null) {
170                mIntent.replaceExtras((Bundle) null);
171                mIntentPersistableExtras = new PersistableBundle(intentExtras);
172            }
173        }
174        mWeight = b.mWeight;
175        mExtras = b.mExtras;
176        updateTimestamp();
177    }
178
179    /**
180     * Throws if any of the mandatory fields is not set.
181     *
182     * @hide
183     */
184    public void enforceMandatoryFields() {
185        Preconditions.checkStringNotEmpty(mId, "Shortcut ID must be provided");
186        Preconditions.checkStringNotEmpty(mTitle, "Shortcut title must be provided");
187        Preconditions.checkNotNull(mIntent, "Shortcut Intent must be provided");
188    }
189
190    /**
191     * Copy constructor.
192     */
193    private ShortcutInfo(ShortcutInfo source, @CloneFlags int cloneFlags) {
194        mId = source.mId;
195        mPackageName = source.mPackageName;
196        mFlags = source.mFlags;
197        mLastChangedTimestamp = source.mLastChangedTimestamp;
198
199        if ((cloneFlags & CLONE_REMOVE_NON_KEY_INFO) == 0) {
200            mActivityComponent = source.mActivityComponent;
201
202            if ((cloneFlags & CLONE_REMOVE_ICON) == 0) {
203                mIcon = source.mIcon;
204                mBitmapPath = source.mBitmapPath;
205                mIconResourceId = source.mIconResourceId;
206            }
207
208            mTitle = source.mTitle;
209            mText = source.mText;
210            if ((cloneFlags & CLONE_REMOVE_INTENT) == 0) {
211                mIntent = source.mIntent;
212                mIntentPersistableExtras = source.mIntentPersistableExtras;
213            }
214            mWeight = source.mWeight;
215            mExtras = source.mExtras;
216        } else {
217            // Set this bit.
218            mFlags |= FLAG_KEY_FIELDS_ONLY;
219        }
220    }
221
222    /**
223     * Copy a {@link ShortcutInfo}, optionally removing fields.
224     * @hide
225     */
226    public ShortcutInfo clone(@CloneFlags int cloneFlags) {
227        return new ShortcutInfo(this, cloneFlags);
228    }
229
230    /**
231     * Copy non-null/zero fields from another {@link ShortcutInfo}.  Only "public" information
232     * will be overwritten.  The timestamp will be updated.
233     *
234     * - Flags will not change
235     * - mBitmapPath will not change
236     * - Current time will be set to timestamp
237     *
238     * @hide
239     */
240    public void copyNonNullFieldsFrom(ShortcutInfo source) {
241        Preconditions.checkState(mId.equals(source.mId), "ID must match");
242        Preconditions.checkState(mPackageName.equals(source.mPackageName),
243                "Package name must match");
244
245        if (source.mActivityComponent != null) {
246            mActivityComponent = source.mActivityComponent;
247        }
248
249        if (source.mIcon != null) {
250            mIcon = source.mIcon;
251        }
252        if (source.mTitle != null) {
253            mTitle = source.mTitle;
254        }
255        if (source.mText != null) {
256            mText = source.mText;
257        }
258        if (source.mIntent != null) {
259            mIntent = source.mIntent;
260            mIntentPersistableExtras = source.mIntentPersistableExtras;
261        }
262        if (source.mWeight != 0) {
263            mWeight = source.mWeight;
264        }
265        if (source.mExtras != null) {
266            mExtras = source.mExtras;
267        }
268
269        updateTimestamp();
270    }
271
272    /**
273     * @hide
274     */
275    public static Icon validateIcon(Icon icon) {
276        switch (icon.getType()) {
277            case Icon.TYPE_RESOURCE:
278            case Icon.TYPE_BITMAP:
279                break; // OK
280            case Icon.TYPE_URI:
281                if (ContentResolver.SCHEME_CONTENT.equals(icon.getUri().getScheme())) {
282                    break;
283                }
284                // Note "file:" is not supported, because depending on the path, system server
285                // cannot access it. // TODO Revisit "file:" icon support
286
287                // fall through
288            default:
289                throw getInvalidIconException();
290        }
291        if (icon.hasTint()) {
292            // TODO support it
293            throw new IllegalArgumentException("Icons with tints are not supported");
294        }
295
296        return icon;
297    }
298
299    /** @hide */
300    public static IllegalArgumentException getInvalidIconException() {
301        return new IllegalArgumentException("Unsupported icon type:"
302                +" only bitmap, resource and content URI are supported");
303    }
304
305    /**
306     * Builder class for {@link ShortcutInfo} objects.
307     */
308    public static class Builder {
309        private final Context mContext;
310
311        private String mId;
312
313        private ComponentName mActivityComponent;
314
315        private Icon mIcon;
316
317        private String mTitle;
318
319        private String mText;
320
321        private Intent mIntent;
322
323        private int mWeight;
324
325        private PersistableBundle mExtras;
326
327        /** Constructor. */
328        public Builder(Context context) {
329            mContext = context;
330        }
331
332        /**
333         * Sets the ID of the shortcut.  This is a mandatory field.
334         */
335        @NonNull
336        public Builder setId(@NonNull String id) {
337            mId = Preconditions.checkStringNotEmpty(id, "id");
338            return this;
339        }
340
341        /**
342         * Optionally sets the target activity.  If it's not set, and if the caller application
343         * has multiple launcher icons, this shortcut will be shown on all those icons.
344         * If it's set, this shortcut will be only shown on this activity.
345         */
346        @NonNull
347        public Builder setActivityComponent(@NonNull ComponentName activityComponent) {
348            mActivityComponent = Preconditions.checkNotNull(activityComponent, "activityComponent");
349            return this;
350        }
351
352        /**
353         * Optionally sets an icon.
354         *
355         * <ul>
356         *     <li>Tints are not supported.
357         *     <li>Bitmaps, resources and "content:" URIs are supported.
358         *     <li>"content:" URI will be fetched when a shortcut is registered to
359         *         {@link ShortcutManager}.  Changing the content from the same URI later will
360         *         not be reflected to launcher icons.
361         * </ul>
362         *
363         * <p>For performance reasons, icons will <b>NOT</b> be available on instances
364         * returned by {@link ShortcutManager} or {@link LauncherApps}.  Launcher applications
365         * need to use {@link LauncherApps#getShortcutIconFd(ShortcutInfo, UserHandle)}
366         * and {@link LauncherApps#getShortcutIconResId(ShortcutInfo, UserHandle)}.
367         */
368        @NonNull
369        public Builder setIcon(Icon icon) {
370            mIcon = validateIcon(icon);
371            return this;
372        }
373
374        /**
375         * Sets the title of a shortcut.  This is a mandatory field.
376         *
377         * <p>This field is intended for a concise description of a shortcut displayed under
378         * an icon.  The recommend max length is 10 characters.
379         */
380        @NonNull
381        public Builder setTitle(@NonNull String title) {
382            mTitle = Preconditions.checkStringNotEmpty(title, "title");
383            return this;
384        }
385
386        /**
387         * Sets the text of a shortcut.  This is an optional field.
388         *
389         * <p>This field is intended to be more descriptive than the shortcut title.
390         * The recommend max length is 25 characters.
391         */
392        @NonNull
393        public Builder setText(@NonNull String text) {
394            mText = Preconditions.checkStringNotEmpty(text, "text");
395            return this;
396        }
397
398        /**
399         * Sets the intent of a shortcut.  This is a mandatory field.  The extras must only contain
400         * persistable information.  (See {@link PersistableBundle}).
401         */
402        @NonNull
403        public Builder setIntent(@NonNull Intent intent) {
404            mIntent = Preconditions.checkNotNull(intent, "intent");
405            return this;
406        }
407
408        /**
409         * Optionally sets the weight of a shortcut, which will be used by the launcher for sorting.
410         * The larger the weight, the more "important" a shortcut is.
411         */
412        @NonNull
413        public Builder setWeight(int weight) {
414            mWeight = weight;
415            return this;
416        }
417
418        /**
419         * Optional values that applications can set.  Applications can store any meta-data of
420         * shortcuts in this, and retrieve later from {@link ShortcutInfo#getExtras()}.
421         */
422        @NonNull
423        public Builder setExtras(@NonNull PersistableBundle extras) {
424            mExtras = extras;
425            return this;
426        }
427
428        /**
429         * Creates a {@link ShortcutInfo} instance.
430         */
431        @NonNull
432        public ShortcutInfo build() {
433            return new ShortcutInfo(this);
434        }
435    }
436
437    /**
438     * Return the ID of the shortcut.
439     */
440    @NonNull
441    public String getId() {
442        return mId;
443    }
444
445    /**
446     * Return the package name of the creator application.
447     */
448    @NonNull
449    public String getPackageName() {
450        return mPackageName;
451    }
452
453    /**
454     * Return the target activity, which may be null, in which case the shortcut is not associated
455     * with a specific activity.
456     */
457    @Nullable
458    public ComponentName getActivityComponent() {
459        return mActivityComponent;
460    }
461
462    /**
463     * Icon.
464     *
465     * For performance reasons, this will <b>NOT</b> be available when an instance is returned
466     * by {@link ShortcutManager} or {@link LauncherApps}.  A launcher application needs to use
467     * other APIs in LauncherApps to fetch the bitmap.
468     *
469     * @hide
470     */
471    @Nullable
472    public Icon getIcon() {
473        return mIcon;
474    }
475
476    /**
477     * Return the shortcut title.
478     *
479     * <p>All shortcuts must have a non-empty title, but this method will return null when
480     * {@link #hasKeyFieldsOnly()} is true.
481     */
482    @Nullable
483    public String getTitle() {
484        return mTitle;
485    }
486
487    /**
488     * Return the shortcut text.
489     */
490    @Nullable
491    public String getText() {
492        return mText;
493    }
494
495    /**
496     * Return the intent.
497     *
498     * <p>All shortcuts must have an intent, but this method will return null when
499     * {@link #hasKeyFieldsOnly()} is true.
500     */
501    @Nullable
502    public Intent getIntent() {
503        if (mIntent == null) {
504            return null;
505        }
506        final Intent intent = new Intent(mIntent);
507        intent.replaceExtras(
508                mIntentPersistableExtras != null ? new Bundle(mIntentPersistableExtras) : null);
509        return intent;
510    }
511
512    /**
513     * Return "raw" intent, which is the original intent without the extras.
514     * @hide
515     */
516    @Nullable
517    public Intent getIntentNoExtras() {
518        return mIntent;
519    }
520
521    /**
522     * The extras in the intent.  We convert extras into {@link PersistableBundle} so we can
523     * persist them.
524     * @hide
525     */
526    @Nullable
527    public PersistableBundle getIntentPersistableExtras() {
528        return mIntentPersistableExtras;
529    }
530
531    /**
532     * Return the weight of a shortcut, which will be used by Launcher for sorting.
533     * The larger the weight, the more "important" a shortcut is.
534     */
535    public int getWeight() {
536        return mWeight;
537    }
538
539    /**
540     * Optional values that application can set.
541     */
542    @Nullable
543    public PersistableBundle getExtras() {
544        return mExtras;
545    }
546
547    /**
548     * Last time when any of the fields was updated.
549     */
550    public long getLastChangedTimestamp() {
551        return mLastChangedTimestamp;
552    }
553
554    /** @hide */
555    @ShortcutFlags
556    public int getFlags() {
557        return mFlags;
558    }
559
560    /** @hide*/
561    public void replaceFlags(@ShortcutFlags int flags) {
562        mFlags = flags;
563    }
564
565    /** @hide*/
566    public void addFlags(@ShortcutFlags int flags) {
567        mFlags |= flags;
568    }
569
570    /** @hide*/
571    public void clearFlags(@ShortcutFlags int flags) {
572        mFlags &= ~flags;
573    }
574
575    /** @hide*/
576    public boolean hasFlags(@ShortcutFlags int flags) {
577        return (mFlags & flags) == flags;
578    }
579
580    /** Return whether a shortcut is dynamic. */
581    public boolean isDynamic() {
582        return hasFlags(FLAG_DYNAMIC);
583    }
584
585    /** Return whether a shortcut is pinned. */
586    public boolean isPinned() {
587        return hasFlags(FLAG_PINNED);
588    }
589
590    /**
591     * Return whether a shortcut's icon is a resource in the owning package.
592     *
593     * @see LauncherApps#getShortcutIconResId(ShortcutInfo, UserHandle)
594     */
595    public boolean hasIconResource() {
596        return hasFlags(FLAG_HAS_ICON_RES);
597    }
598
599    /**
600     * Return whether a shortcut's icon is stored as a file.
601     *
602     * @see LauncherApps#getShortcutIconFd(ShortcutInfo, UserHandle)
603     */
604    public boolean hasIconFile() {
605        return hasFlags(FLAG_HAS_ICON_FILE);
606    }
607
608    /**
609     * Return whether a shortcut only contains "key" information only or not.  If true, only the
610     * following fields are available.
611     * <ul>
612     *     <li>{@link #getId()}
613     *     <li>{@link #getPackageName()}
614     *     <li>{@link #getLastChangedTimestamp()}
615     *     <li>{@link #isDynamic()}
616     *     <li>{@link #isPinned()}
617     *     <li>{@link #hasIconResource()}
618     *     <li>{@link #hasIconFile()}
619     * </ul>
620     */
621    public boolean hasKeyFieldsOnly() {
622        return hasFlags(FLAG_KEY_FIELDS_ONLY);
623    }
624
625    /** @hide */
626    public void updateTimestamp() {
627        mLastChangedTimestamp = System.currentTimeMillis();
628    }
629
630    /** @hide */
631    // VisibleForTesting
632    public void setTimestamp(long value) {
633        mLastChangedTimestamp = value;
634    }
635
636    /** @hide */
637    public void clearIcon() {
638        mIcon = null;
639    }
640
641    /** @hide */
642    public void setIconResourceId(int iconResourceId) {
643        mIconResourceId = iconResourceId;
644    }
645
646    /** @hide */
647    public int getIconResourceId() {
648        return mIconResourceId;
649    }
650
651    /** @hide */
652    public String getBitmapPath() {
653        return mBitmapPath;
654    }
655
656    /** @hide */
657    public void setBitmapPath(String bitmapPath) {
658        mBitmapPath = bitmapPath;
659    }
660
661    private ShortcutInfo(Parcel source) {
662        final ClassLoader cl = getClass().getClassLoader();
663
664        mId = source.readString();
665        mPackageName = source.readString();
666        mActivityComponent = source.readParcelable(cl);
667        mIcon = source.readParcelable(cl);
668        mTitle = source.readString();
669        mText = source.readString();
670        mIntent = source.readParcelable(cl);
671        mIntentPersistableExtras = source.readParcelable(cl);
672        mWeight = source.readInt();
673        mExtras = source.readParcelable(cl);
674        mLastChangedTimestamp = source.readLong();
675        mFlags = source.readInt();
676        mIconResourceId = source.readInt();
677        mBitmapPath = source.readString();
678    }
679
680    @Override
681    public void writeToParcel(Parcel dest, int flags) {
682        dest.writeString(mId);
683        dest.writeString(mPackageName);
684        dest.writeParcelable(mActivityComponent, flags);
685        dest.writeParcelable(mIcon, flags);
686        dest.writeString(mTitle);
687        dest.writeString(mText);
688        dest.writeParcelable(mIntent, flags);
689        dest.writeParcelable(mIntentPersistableExtras, flags);
690        dest.writeInt(mWeight);
691        dest.writeParcelable(mExtras, flags);
692        dest.writeLong(mLastChangedTimestamp);
693        dest.writeInt(mFlags);
694        dest.writeInt(mIconResourceId);
695        dest.writeString(mBitmapPath);
696    }
697
698    public static final Creator<ShortcutInfo> CREATOR =
699            new Creator<ShortcutInfo>() {
700                public ShortcutInfo createFromParcel(Parcel source) {
701                    return new ShortcutInfo(source);
702                }
703                public ShortcutInfo[] newArray(int size) {
704                    return new ShortcutInfo[size];
705                }
706            };
707
708    @Override
709    public int describeContents() {
710        return 0;
711    }
712
713    /**
714     * Return a string representation, intended for logging.  Some fields will be retracted.
715     */
716    @Override
717    public String toString() {
718        return toStringInner(/* secure =*/ true, /* includeInternalData =*/ false);
719    }
720
721    /** @hide */
722    public String toInsecureString() {
723        return toStringInner(/* secure =*/ false, /* includeInternalData =*/ true);
724    }
725
726    private String toStringInner(boolean secure, boolean includeInternalData) {
727        final StringBuilder sb = new StringBuilder();
728        sb.append("ShortcutInfo {");
729
730        sb.append("id=");
731        sb.append(secure ? "***" : mId);
732
733        sb.append(", packageName=");
734        sb.append(mPackageName);
735
736        if (isDynamic()) {
737            sb.append(", dynamic");
738        }
739        if (isPinned()) {
740            sb.append(", pinned");
741        }
742
743        sb.append(", activity=");
744        sb.append(mActivityComponent);
745
746        sb.append(", title=");
747        sb.append(secure ? "***" : mTitle);
748
749        sb.append(", text=");
750        sb.append(secure ? "***" : mText);
751
752        sb.append(", icon=");
753        sb.append(mIcon);
754
755        sb.append(", weight=");
756        sb.append(mWeight);
757
758        sb.append(", timestamp=");
759        sb.append(mLastChangedTimestamp);
760
761        sb.append(", intent=");
762        sb.append(mIntent);
763
764        sb.append(", intentExtras=");
765        sb.append(secure ? "***" : mIntentPersistableExtras);
766
767        sb.append(", extras=");
768        sb.append(mExtras);
769
770        sb.append(", flags=");
771        sb.append(mFlags);
772
773        if (includeInternalData) {
774
775            sb.append(", iconRes=");
776            sb.append(mIconResourceId);
777
778            sb.append(", bitmapPath=");
779            sb.append(mBitmapPath);
780        }
781
782        sb.append("}");
783        return sb.toString();
784    }
785
786    /** @hide */
787    public ShortcutInfo(String id, String packageName, ComponentName activityComponent,
788            Icon icon, String title, String text, Intent intent,
789            PersistableBundle intentPersistableExtras,
790            int weight, PersistableBundle extras, long lastChangedTimestamp,
791            int flags, int iconResId, String bitmapPath) {
792        mId = id;
793        mPackageName = packageName;
794        mActivityComponent = activityComponent;
795        mIcon = icon;
796        mTitle = title;
797        mText = text;
798        mIntent = intent;
799        mIntentPersistableExtras = intentPersistableExtras;
800        mWeight = weight;
801        mExtras = extras;
802        mLastChangedTimestamp = lastChangedTimestamp;
803        mFlags = flags;
804        mIconResourceId = iconResId;
805        mBitmapPath = bitmapPath;
806    }
807}
808