• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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 static android.content.res.Resources.ID_NULL;
20 
21 import android.annotation.DrawableRes;
22 import android.annotation.IntDef;
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.annotation.StringRes;
26 import android.annotation.SystemApi;
27 import android.content.res.ResourceId;
28 import android.os.Parcel;
29 import android.os.Parcelable;
30 import android.os.PersistableBundle;
31 import android.util.Slog;
32 import android.util.TypedXmlPullParser;
33 import android.util.TypedXmlSerializer;
34 
35 import com.android.internal.util.Preconditions;
36 import com.android.internal.util.XmlUtils;
37 
38 import java.io.IOException;
39 import java.lang.annotation.Retention;
40 import java.lang.annotation.RetentionPolicy;
41 import java.util.Locale;
42 import java.util.Objects;
43 
44 /**
45  * A container to describe the dialog to be shown when the user tries to launch a suspended
46  * application. The suspending app can customize the dialog's following attributes:
47  * <ul>
48  * <li>The dialog icon, by providing a resource id.
49  * <li>The title text, by providing a resource id.
50  * <li>The text of the dialog's body, by providing a resource id or a string.
51  * <li>The text on the neutral button by providing a resource id.
52  * <li>The action performed on tapping the neutral button. Only {@link #BUTTON_ACTION_UNSUSPEND}
53  * and {@link #BUTTON_ACTION_MORE_DETAILS} are currently supported.
54  * </ul>
55  * System defaults are used whenever any of these are not provided, or any of the provided resource
56  * ids cannot be resolved at the time of displaying the dialog.
57  *
58  * @hide
59  * @see PackageManager#setPackagesSuspended(String[], boolean, PersistableBundle, PersistableBundle,
60  * SuspendDialogInfo)
61  * @see Builder
62  */
63 @SystemApi
64 public final class SuspendDialogInfo implements Parcelable {
65     private static final String TAG = SuspendDialogInfo.class.getSimpleName();
66     private static final String XML_ATTR_ICON_RES_ID = "iconResId";
67     private static final String XML_ATTR_TITLE_RES_ID = "titleResId";
68     private static final String XML_ATTR_TITLE = "title";
69     private static final String XML_ATTR_DIALOG_MESSAGE_RES_ID = "dialogMessageResId";
70     private static final String XML_ATTR_DIALOG_MESSAGE = "dialogMessage";
71     private static final String XML_ATTR_BUTTON_TEXT_RES_ID = "buttonTextResId";
72     private static final String XML_ATTR_BUTTON_TEXT = "buttonText";
73     private static final String XML_ATTR_BUTTON_ACTION = "buttonAction";
74 
75     private final int mIconResId;
76     private final int mTitleResId;
77     private final String mTitle;
78     private final int mDialogMessageResId;
79     private final String mDialogMessage;
80     private final int mNeutralButtonTextResId;
81     private final String mNeutralButtonText;
82     private final int mNeutralButtonAction;
83 
84     /**
85      * Used with {@link Builder#setNeutralButtonAction(int)} to create a neutral button that
86      * starts the {@link android.content.Intent#ACTION_SHOW_SUSPENDED_APP_DETAILS} activity.
87      * @see Builder#setNeutralButtonAction(int)
88      */
89     public static final int BUTTON_ACTION_MORE_DETAILS = 0;
90 
91     /**
92      * Used with {@link Builder#setNeutralButtonAction(int)} to create a neutral button that
93      * unsuspends the app that the user was trying to launch and continues with the launch. The
94      * system also sends the broadcast
95      * {@link android.content.Intent#ACTION_PACKAGE_UNSUSPENDED_MANUALLY} to the suspending app
96      * when this happens.
97      * @see Builder#setNeutralButtonAction(int)
98      * @see android.content.Intent#ACTION_PACKAGE_UNSUSPENDED_MANUALLY
99      */
100     public static final int BUTTON_ACTION_UNSUSPEND = 1;
101 
102     /**
103      * Button actions to specify what happens when the user taps on the neutral button.
104      * To be used with {@link Builder#setNeutralButtonAction(int)}.
105      *
106      * @hide
107      * @see Builder#setNeutralButtonAction(int)
108      */
109     @IntDef(flag = true, prefix = {"BUTTON_ACTION_"}, value = {
110             BUTTON_ACTION_MORE_DETAILS,
111             BUTTON_ACTION_UNSUSPEND
112     })
113     @Retention(RetentionPolicy.SOURCE)
114     public @interface ButtonAction {
115     }
116 
117     /**
118      * @return the resource id of the icon to be used with the dialog
119      * @hide
120      */
121     @DrawableRes
getIconResId()122     public int getIconResId() {
123         return mIconResId;
124     }
125 
126     /**
127      * @return the resource id of the title to be used with the dialog
128      * @hide
129      */
130     @StringRes
getTitleResId()131     public int getTitleResId() {
132         return mTitleResId;
133     }
134 
135     /**
136      * @return the title to be shown on the dialog. Returns {@code null} if {@link #getTitleResId()}
137      * returns a valid resource id
138      * @hide
139      */
140     @Nullable
getTitle()141     public String getTitle() {
142         return mTitle;
143     }
144 
145     /**
146      * @return the resource id of the text to be shown in the dialog's body
147      * @hide
148      */
149     @StringRes
getDialogMessageResId()150     public int getDialogMessageResId() {
151         return mDialogMessageResId;
152     }
153 
154     /**
155      * @return the text to be shown in the dialog's body. Returns {@code null} if {@link
156      * #getDialogMessageResId()} returns a valid resource id
157      * @hide
158      */
159     @Nullable
getDialogMessage()160     public String getDialogMessage() {
161         return mDialogMessage;
162     }
163 
164     /**
165      * @return the text to be shown on the neutral button
166      * @hide
167      */
168     @StringRes
getNeutralButtonTextResId()169     public int getNeutralButtonTextResId() {
170         return mNeutralButtonTextResId;
171     }
172 
173     /**
174      * @return the text to be shown on the neutral button. Returns {@code null} if
175      * {@link #getNeutralButtonTextResId()} returns a valid resource id
176      * @hide
177      */
178     @Nullable
getNeutralButtonText()179     public String getNeutralButtonText() {
180         return mNeutralButtonText;
181     }
182 
183     /**
184      * @return The {@link ButtonAction} that happens on tapping this button
185      * @hide
186      */
187     @ButtonAction
getNeutralButtonAction()188     public int getNeutralButtonAction() {
189         return mNeutralButtonAction;
190     }
191 
192     /**
193      * @hide
194      */
saveToXml(TypedXmlSerializer out)195     public void saveToXml(TypedXmlSerializer out) throws IOException {
196         if (mIconResId != ID_NULL) {
197             out.attributeInt(null, XML_ATTR_ICON_RES_ID, mIconResId);
198         }
199         if (mTitleResId != ID_NULL) {
200             out.attributeInt(null, XML_ATTR_TITLE_RES_ID, mTitleResId);
201         } else {
202             XmlUtils.writeStringAttribute(out, XML_ATTR_TITLE, mTitle);
203         }
204         if (mDialogMessageResId != ID_NULL) {
205             out.attributeInt(null, XML_ATTR_DIALOG_MESSAGE_RES_ID, mDialogMessageResId);
206         } else {
207             XmlUtils.writeStringAttribute(out, XML_ATTR_DIALOG_MESSAGE, mDialogMessage);
208         }
209         if (mNeutralButtonTextResId != ID_NULL) {
210             out.attributeInt(null, XML_ATTR_BUTTON_TEXT_RES_ID, mNeutralButtonTextResId);
211         } else {
212             XmlUtils.writeStringAttribute(out, XML_ATTR_BUTTON_TEXT, mNeutralButtonText);
213         }
214         out.attributeInt(null, XML_ATTR_BUTTON_ACTION, mNeutralButtonAction);
215     }
216 
217     /**
218      * @hide
219      */
restoreFromXml(TypedXmlPullParser in)220     public static SuspendDialogInfo restoreFromXml(TypedXmlPullParser in) {
221         final SuspendDialogInfo.Builder dialogInfoBuilder = new SuspendDialogInfo.Builder();
222         try {
223             final int iconId = in.getAttributeInt(null, XML_ATTR_ICON_RES_ID, ID_NULL);
224             final int titleId = in.getAttributeInt(null, XML_ATTR_TITLE_RES_ID, ID_NULL);
225             final String title = XmlUtils.readStringAttribute(in, XML_ATTR_TITLE);
226             final int buttonTextId =
227                     in.getAttributeInt(null, XML_ATTR_BUTTON_TEXT_RES_ID, ID_NULL);
228             final String buttonText = XmlUtils.readStringAttribute(in, XML_ATTR_BUTTON_TEXT);
229             final int buttonAction =
230                     in.getAttributeInt(null, XML_ATTR_BUTTON_ACTION, BUTTON_ACTION_MORE_DETAILS);
231             final int dialogMessageResId =
232                     in.getAttributeInt(null, XML_ATTR_DIALOG_MESSAGE_RES_ID, ID_NULL);
233             final String dialogMessage = XmlUtils.readStringAttribute(in, XML_ATTR_DIALOG_MESSAGE);
234 
235             if (iconId != ID_NULL) {
236                 dialogInfoBuilder.setIcon(iconId);
237             }
238             if (titleId != ID_NULL) {
239                 dialogInfoBuilder.setTitle(titleId);
240             } else if (title != null) {
241                 dialogInfoBuilder.setTitle(title);
242             }
243             if (buttonTextId != ID_NULL) {
244                 dialogInfoBuilder.setNeutralButtonText(buttonTextId);
245             } else if (buttonText != null) {
246                 dialogInfoBuilder.setNeutralButtonText(buttonText);
247             }
248             if (dialogMessageResId != ID_NULL) {
249                 dialogInfoBuilder.setMessage(dialogMessageResId);
250             } else if (dialogMessage != null) {
251                 dialogInfoBuilder.setMessage(dialogMessage);
252             }
253             dialogInfoBuilder.setNeutralButtonAction(buttonAction);
254         } catch (Exception e) {
255             Slog.e(TAG, "Exception while parsing from xml. Some fields may default", e);
256         }
257         return dialogInfoBuilder.build();
258     }
259 
260     @Override
hashCode()261     public int hashCode() {
262         int hashCode = mIconResId;
263         hashCode = 31 * hashCode + mTitleResId;
264         hashCode = 31 * hashCode + Objects.hashCode(mTitle);
265         hashCode = 31 * hashCode + mNeutralButtonTextResId;
266         hashCode = 31 * hashCode + Objects.hashCode(mNeutralButtonText);
267         hashCode = 31 * hashCode + mDialogMessageResId;
268         hashCode = 31 * hashCode + Objects.hashCode(mDialogMessage);
269         hashCode = 31 * hashCode + mNeutralButtonAction;
270         return hashCode;
271     }
272 
273     @Override
equals(@ullable Object obj)274     public boolean equals(@Nullable Object obj) {
275         if (this == obj) {
276             return true;
277         }
278         if (!(obj instanceof SuspendDialogInfo)) {
279             return false;
280         }
281         final SuspendDialogInfo otherDialogInfo = (SuspendDialogInfo) obj;
282         return mIconResId == otherDialogInfo.mIconResId
283                 && mTitleResId == otherDialogInfo.mTitleResId
284                 && Objects.equals(mTitle, otherDialogInfo.mTitle)
285                 && mDialogMessageResId == otherDialogInfo.mDialogMessageResId
286                 && Objects.equals(mDialogMessage, otherDialogInfo.mDialogMessage)
287                 && mNeutralButtonTextResId == otherDialogInfo.mNeutralButtonTextResId
288                 && Objects.equals(mNeutralButtonText, otherDialogInfo.mNeutralButtonText)
289                 && mNeutralButtonAction == otherDialogInfo.mNeutralButtonAction;
290     }
291 
292     @NonNull
293     @Override
toString()294     public String toString() {
295         final StringBuilder builder = new StringBuilder("SuspendDialogInfo: {");
296         if (mIconResId != ID_NULL) {
297             builder.append("mIconId = 0x");
298             builder.append(Integer.toHexString(mIconResId));
299             builder.append(" ");
300         }
301         if (mTitleResId != ID_NULL) {
302             builder.append("mTitleResId = 0x");
303             builder.append(Integer.toHexString(mTitleResId));
304             builder.append(" ");
305         } else if (mTitle != null) {
306             builder.append("mTitle = \"");
307             builder.append(mTitle);
308             builder.append("\"");
309         }
310         if (mNeutralButtonTextResId != ID_NULL) {
311             builder.append("mNeutralButtonTextResId = 0x");
312             builder.append(Integer.toHexString(mNeutralButtonTextResId));
313             builder.append(" ");
314         } else if (mNeutralButtonText != null) {
315             builder.append("mNeutralButtonText = \"");
316             builder.append(mNeutralButtonText);
317             builder.append("\"");
318         }
319         if (mDialogMessageResId != ID_NULL) {
320             builder.append("mDialogMessageResId = 0x");
321             builder.append(Integer.toHexString(mDialogMessageResId));
322             builder.append(" ");
323         } else if (mDialogMessage != null) {
324             builder.append("mDialogMessage = \"");
325             builder.append(mDialogMessage);
326             builder.append("\" ");
327         }
328         builder.append("mNeutralButtonAction = ");
329         builder.append(mNeutralButtonAction);
330         builder.append("}");
331         return builder.toString();
332     }
333 
334     @Override
describeContents()335     public int describeContents() {
336         return 0;
337     }
338 
339     @Override
writeToParcel(Parcel dest, int parcelableFlags)340     public void writeToParcel(Parcel dest, int parcelableFlags) {
341         dest.writeInt(mIconResId);
342         dest.writeInt(mTitleResId);
343         dest.writeString(mTitle);
344         dest.writeInt(mDialogMessageResId);
345         dest.writeString(mDialogMessage);
346         dest.writeInt(mNeutralButtonTextResId);
347         dest.writeString(mNeutralButtonText);
348         dest.writeInt(mNeutralButtonAction);
349     }
350 
SuspendDialogInfo(Parcel source)351     private SuspendDialogInfo(Parcel source) {
352         mIconResId = source.readInt();
353         mTitleResId = source.readInt();
354         mTitle = source.readString();
355         mDialogMessageResId = source.readInt();
356         mDialogMessage = source.readString();
357         mNeutralButtonTextResId = source.readInt();
358         mNeutralButtonText = source.readString();
359         mNeutralButtonAction = source.readInt();
360     }
361 
SuspendDialogInfo(Builder b)362     SuspendDialogInfo(Builder b) {
363         mIconResId = b.mIconResId;
364         mTitleResId = b.mTitleResId;
365         mTitle = (mTitleResId == ID_NULL) ? b.mTitle : null;
366         mDialogMessageResId = b.mDialogMessageResId;
367         mDialogMessage = (mDialogMessageResId == ID_NULL) ? b.mDialogMessage : null;
368         mNeutralButtonTextResId = b.mNeutralButtonTextResId;
369         mNeutralButtonText = (mNeutralButtonTextResId == ID_NULL) ? b.mNeutralButtonText : null;
370         mNeutralButtonAction = b.mNeutralButtonAction;
371     }
372 
373     public static final @NonNull Creator<SuspendDialogInfo> CREATOR =
374             new Creator<SuspendDialogInfo>() {
375         @Override
376         public SuspendDialogInfo createFromParcel(Parcel source) {
377             return new SuspendDialogInfo(source);
378         }
379 
380         @Override
381         public SuspendDialogInfo[] newArray(int size) {
382             return new SuspendDialogInfo[size];
383         }
384     };
385 
386     /**
387      * Builder to build a {@link SuspendDialogInfo} object.
388      */
389     public static final class Builder {
390         private int mDialogMessageResId = ID_NULL;
391         private String mDialogMessage;
392         private int mTitleResId = ID_NULL;
393         private String mTitle;
394         private int mIconResId = ID_NULL;
395         private int mNeutralButtonTextResId = ID_NULL;
396         private String mNeutralButtonText;
397         private int mNeutralButtonAction = BUTTON_ACTION_MORE_DETAILS;
398 
399         /**
400          * Set the resource id of the icon to be used. If not provided, no icon will be shown.
401          *
402          * @param resId The resource id of the icon.
403          * @return this builder object.
404          */
405         @NonNull
setIcon(@rawableRes int resId)406         public Builder setIcon(@DrawableRes int resId) {
407             Preconditions.checkArgument(ResourceId.isValid(resId), "Invalid resource id provided");
408             mIconResId = resId;
409             return this;
410         }
411 
412         /**
413          * Set the resource id of the title text to be displayed. If this is not provided, the
414          * system will use a default title.
415          *
416          * @param resId The resource id of the title.
417          * @return this builder object.
418          */
419         @NonNull
setTitle(@tringRes int resId)420         public Builder setTitle(@StringRes int resId) {
421             Preconditions.checkArgument(ResourceId.isValid(resId), "Invalid resource id provided");
422             mTitleResId = resId;
423             return this;
424         }
425 
426         /**
427          * Set the title text of the dialog. Ignored if a resource id is set via
428          * {@link #setTitle(int)}
429          *
430          * @param title The title of the dialog.
431          * @return this builder object.
432          * @see #setTitle(int)
433          */
434         @NonNull
setTitle(@onNull String title)435         public Builder setTitle(@NonNull String title) {
436             Preconditions.checkStringNotEmpty(title, "Title cannot be null or empty");
437             mTitle = title;
438             return this;
439         }
440 
441         /**
442          * Set the text to show in the body of the dialog. Ignored if a resource id is set via
443          * {@link #setMessage(int)}.
444          * <p>
445          * The system will use {@link String#format(Locale, String, Object...) String.format} to
446          * insert the suspended app name into the message, so an example format string could be
447          * {@code "The app %1$s is currently suspended"}. This is optional - if the string passed in
448          * {@code message} does not accept an argument, it will be used as is.
449          *
450          * @param message The dialog message.
451          * @return this builder object.
452          * @see #setMessage(int)
453          */
454         @NonNull
setMessage(@onNull String message)455         public Builder setMessage(@NonNull String message) {
456             Preconditions.checkStringNotEmpty(message, "Message cannot be null or empty");
457             mDialogMessage = message;
458             return this;
459         }
460 
461         /**
462          * Set the resource id of the dialog message to be shown. If no dialog message is provided
463          * via either this method or {@link #setMessage(String)}, the system will use a default
464          * message.
465          * <p>
466          * The system will use {@link android.content.res.Resources#getString(int, Object...)
467          * getString} to insert the suspended app name into the message, so an example format string
468          * could be {@code "The app %1$s is currently suspended"}. This is optional - if the string
469          * referred to by {@code resId} does not accept an argument, it will be used as is.
470          *
471          * @param resId The resource id of the dialog message.
472          * @return this builder object.
473          * @see #setMessage(String)
474          */
475         @NonNull
setMessage(@tringRes int resId)476         public Builder setMessage(@StringRes int resId) {
477             Preconditions.checkArgument(ResourceId.isValid(resId), "Invalid resource id provided");
478             mDialogMessageResId = resId;
479             return this;
480         }
481 
482         /**
483          * Set the resource id of text to be shown on the neutral button. Tapping this button would
484          * perform the {@link ButtonAction action} specified through
485          * {@link #setNeutralButtonAction(int)}. If this is not provided, the system will use a
486          * default text.
487          *
488          * @param resId The resource id of the button text
489          * @return this builder object.
490          */
491         @NonNull
setNeutralButtonText(@tringRes int resId)492         public Builder setNeutralButtonText(@StringRes int resId) {
493             Preconditions.checkArgument(ResourceId.isValid(resId), "Invalid resource id provided");
494             mNeutralButtonTextResId = resId;
495             return this;
496         }
497 
498         /**
499          * Set the text to be shown on the neutral button. Ignored if a resource id is set via
500          * {@link #setNeutralButtonText(int)}
501          *
502          * @param neutralButtonText The title of the dialog.
503          * @return this builder object.
504          * @see #setNeutralButtonText(int)
505          */
506         @NonNull
setNeutralButtonText(@onNull String neutralButtonText)507         public Builder setNeutralButtonText(@NonNull String neutralButtonText) {
508             Preconditions.checkStringNotEmpty(neutralButtonText,
509                     "Button text cannot be null or empty");
510             mNeutralButtonText = neutralButtonText;
511             return this;
512         }
513 
514         /**
515          * Set the action expected to happen on neutral button tap. Defaults to
516          * {@link #BUTTON_ACTION_MORE_DETAILS} if this is not provided.
517          *
518          * @param buttonAction Either {@link #BUTTON_ACTION_MORE_DETAILS} or
519          *                     {@link #BUTTON_ACTION_UNSUSPEND}.
520          * @return this builder object
521          */
522         @NonNull
setNeutralButtonAction(@uttonAction int buttonAction)523         public Builder setNeutralButtonAction(@ButtonAction int buttonAction) {
524             Preconditions.checkArgument(buttonAction == BUTTON_ACTION_MORE_DETAILS
525                     || buttonAction == BUTTON_ACTION_UNSUSPEND, "Invalid button action");
526             mNeutralButtonAction = buttonAction;
527             return this;
528         }
529 
530         /**
531          * Build the final object based on given inputs.
532          *
533          * @return The {@link SuspendDialogInfo} object built using this builder.
534          */
535         @NonNull
build()536         public SuspendDialogInfo build() {
537             return new SuspendDialogInfo(this);
538         }
539     }
540 }
541