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.NonNull;
23 import android.annotation.Nullable;
24 import android.annotation.StringRes;
25 import android.annotation.SystemApi;
26 import android.content.res.ResourceId;
27 import android.os.Parcel;
28 import android.os.Parcelable;
29 import android.os.PersistableBundle;
30 import android.util.Slog;
31 
32 import com.android.internal.util.Preconditions;
33 import com.android.internal.util.XmlUtils;
34 
35 import org.xmlpull.v1.XmlPullParser;
36 import org.xmlpull.v1.XmlSerializer;
37 
38 import java.io.IOException;
39 import java.util.Locale;
40 import java.util.Objects;
41 
42 /**
43  * A container to describe the dialog to be shown when the user tries to launch a suspended
44  * application.
45  * The suspending app can customize the dialog's following attributes:
46  * <ul>
47  * <li>The dialog icon, by providing a resource id.
48  * <li>The title text, by providing a resource id.
49  * <li>The text of the dialog's body, by providing a resource id or a string.
50  * <li>The text on the neutral button which starts the
51  * {@link android.content.Intent#ACTION_SHOW_SUSPENDED_APP_DETAILS SHOW_SUSPENDED_APP_DETAILS}
52  * activity, by providing a resource id.
53  * </ul>
54  * System defaults are used whenever any of these are not provided, or any of the provided resource
55  * ids cannot be resolved at the time of displaying the dialog.
56  *
57  * @hide
58  * @see PackageManager#setPackagesSuspended(String[], boolean, PersistableBundle, PersistableBundle,
59  * SuspendDialogInfo)
60  * @see Builder
61  */
62 @SystemApi
63 public final class SuspendDialogInfo implements Parcelable {
64     private static final String TAG = SuspendDialogInfo.class.getSimpleName();
65     private static final String XML_ATTR_ICON_RES_ID = "iconResId";
66     private static final String XML_ATTR_TITLE_RES_ID = "titleResId";
67     private static final String XML_ATTR_DIALOG_MESSAGE_RES_ID = "dialogMessageResId";
68     private static final String XML_ATTR_DIALOG_MESSAGE = "dialogMessage";
69     private static final String XML_ATTR_BUTTON_TEXT_RES_ID = "buttonTextResId";
70 
71     private final int mIconResId;
72     private final int mTitleResId;
73     private final int mDialogMessageResId;
74     private final String mDialogMessage;
75     private final int mNeutralButtonTextResId;
76 
77     /**
78      * @return the resource id of the icon to be used with the dialog
79      * @hide
80      */
81     @DrawableRes
getIconResId()82     public int getIconResId() {
83         return mIconResId;
84     }
85 
86     /**
87      * @return the resource id of the title to be used with the dialog
88      * @hide
89      */
90     @StringRes
getTitleResId()91     public int getTitleResId() {
92         return mTitleResId;
93     }
94 
95     /**
96      * @return the resource id of the text to be shown in the dialog's body
97      * @hide
98      */
99     @StringRes
getDialogMessageResId()100     public int getDialogMessageResId() {
101         return mDialogMessageResId;
102     }
103 
104     /**
105      * @return the text to be shown in the dialog's body. Returns {@code null} if
106      * {@link #getDialogMessageResId()} returns a valid resource id.
107      * @hide
108      */
109     @Nullable
getDialogMessage()110     public String getDialogMessage() {
111         return mDialogMessage;
112     }
113 
114     /**
115      * @return the text to be shown
116      * @hide
117      */
118     @StringRes
getNeutralButtonTextResId()119     public int getNeutralButtonTextResId() {
120         return mNeutralButtonTextResId;
121     }
122 
123     /**
124      * @hide
125      */
saveToXml(XmlSerializer out)126     public void saveToXml(XmlSerializer out) throws IOException {
127         if (mIconResId != ID_NULL) {
128             XmlUtils.writeIntAttribute(out, XML_ATTR_ICON_RES_ID, mIconResId);
129         }
130         if (mTitleResId != ID_NULL) {
131             XmlUtils.writeIntAttribute(out, XML_ATTR_TITLE_RES_ID, mTitleResId);
132         }
133         if (mDialogMessageResId != ID_NULL) {
134             XmlUtils.writeIntAttribute(out, XML_ATTR_DIALOG_MESSAGE_RES_ID, mDialogMessageResId);
135         } else {
136             XmlUtils.writeStringAttribute(out, XML_ATTR_DIALOG_MESSAGE, mDialogMessage);
137         }
138         if (mNeutralButtonTextResId != ID_NULL) {
139             XmlUtils.writeIntAttribute(out, XML_ATTR_BUTTON_TEXT_RES_ID, mNeutralButtonTextResId);
140         }
141     }
142 
143     /**
144      * @hide
145      */
restoreFromXml(XmlPullParser in)146     public static SuspendDialogInfo restoreFromXml(XmlPullParser in) {
147         final SuspendDialogInfo.Builder dialogInfoBuilder = new SuspendDialogInfo.Builder();
148         try {
149             final int iconId = XmlUtils.readIntAttribute(in, XML_ATTR_ICON_RES_ID, ID_NULL);
150             final int titleId = XmlUtils.readIntAttribute(in, XML_ATTR_TITLE_RES_ID, ID_NULL);
151             final int buttonTextId = XmlUtils.readIntAttribute(in, XML_ATTR_BUTTON_TEXT_RES_ID,
152                     ID_NULL);
153             final int dialogMessageResId = XmlUtils.readIntAttribute(
154                     in, XML_ATTR_DIALOG_MESSAGE_RES_ID, ID_NULL);
155             final String dialogMessage = XmlUtils.readStringAttribute(in, XML_ATTR_DIALOG_MESSAGE);
156 
157             if (iconId != ID_NULL) {
158                 dialogInfoBuilder.setIcon(iconId);
159             }
160             if (titleId != ID_NULL) {
161                 dialogInfoBuilder.setTitle(titleId);
162             }
163             if (buttonTextId != ID_NULL) {
164                 dialogInfoBuilder.setNeutralButtonText(buttonTextId);
165             }
166             if (dialogMessageResId != ID_NULL) {
167                 dialogInfoBuilder.setMessage(dialogMessageResId);
168             } else if (dialogMessage != null) {
169                 dialogInfoBuilder.setMessage(dialogMessage);
170             }
171         } catch (Exception e) {
172             Slog.e(TAG, "Exception while parsing from xml. Some fields may default", e);
173         }
174         return dialogInfoBuilder.build();
175     }
176 
177     @Override
hashCode()178     public int hashCode() {
179         int hashCode = mIconResId;
180         hashCode = 31 * hashCode + mTitleResId;
181         hashCode = 31 * hashCode + mNeutralButtonTextResId;
182         hashCode = 31 * hashCode + mDialogMessageResId;
183         hashCode = 31 * hashCode + Objects.hashCode(mDialogMessage);
184         return hashCode;
185     }
186 
187     @Override
equals(@ullable Object obj)188     public boolean equals(@Nullable Object obj) {
189         if (this == obj) {
190             return true;
191         }
192         if (!(obj instanceof SuspendDialogInfo)) {
193             return false;
194         }
195         final SuspendDialogInfo otherDialogInfo = (SuspendDialogInfo) obj;
196         return mIconResId == otherDialogInfo.mIconResId
197                 && mTitleResId == otherDialogInfo.mTitleResId
198                 && mDialogMessageResId == otherDialogInfo.mDialogMessageResId
199                 && mNeutralButtonTextResId == otherDialogInfo.mNeutralButtonTextResId
200                 && Objects.equals(mDialogMessage, otherDialogInfo.mDialogMessage);
201     }
202 
203     @NonNull
204     @Override
toString()205     public String toString() {
206         final StringBuilder builder = new StringBuilder("SuspendDialogInfo: {");
207         if (mIconResId != ID_NULL) {
208             builder.append("mIconId = 0x");
209             builder.append(Integer.toHexString(mIconResId));
210             builder.append(" ");
211         }
212         if (mTitleResId != ID_NULL) {
213             builder.append("mTitleResId = 0x");
214             builder.append(Integer.toHexString(mTitleResId));
215             builder.append(" ");
216         }
217         if (mNeutralButtonTextResId != ID_NULL) {
218             builder.append("mNeutralButtonTextResId = 0x");
219             builder.append(Integer.toHexString(mNeutralButtonTextResId));
220             builder.append(" ");
221         }
222         if (mDialogMessageResId != ID_NULL) {
223             builder.append("mDialogMessageResId = 0x");
224             builder.append(Integer.toHexString(mDialogMessageResId));
225             builder.append(" ");
226         } else if (mDialogMessage != null) {
227             builder.append("mDialogMessage = \"");
228             builder.append(mDialogMessage);
229             builder.append("\" ");
230         }
231         builder.append("}");
232         return builder.toString();
233     }
234 
235     @Override
describeContents()236     public int describeContents() {
237         return 0;
238     }
239 
240     @Override
writeToParcel(Parcel dest, int parcelableFlags)241     public void writeToParcel(Parcel dest, int parcelableFlags) {
242         dest.writeInt(mIconResId);
243         dest.writeInt(mTitleResId);
244         dest.writeInt(mDialogMessageResId);
245         dest.writeString(mDialogMessage);
246         dest.writeInt(mNeutralButtonTextResId);
247     }
248 
SuspendDialogInfo(Parcel source)249     private SuspendDialogInfo(Parcel source) {
250         mIconResId = source.readInt();
251         mTitleResId = source.readInt();
252         mDialogMessageResId = source.readInt();
253         mDialogMessage = source.readString();
254         mNeutralButtonTextResId = source.readInt();
255     }
256 
SuspendDialogInfo(Builder b)257     SuspendDialogInfo(Builder b) {
258         mIconResId = b.mIconResId;
259         mTitleResId = b.mTitleResId;
260         mDialogMessageResId = b.mDialogMessageResId;
261         mDialogMessage = (mDialogMessageResId == ID_NULL) ? b.mDialogMessage : null;
262         mNeutralButtonTextResId = b.mNeutralButtonTextResId;
263     }
264 
265     public static final @android.annotation.NonNull Creator<SuspendDialogInfo> CREATOR = new Creator<SuspendDialogInfo>() {
266         @Override
267         public SuspendDialogInfo createFromParcel(Parcel source) {
268             return new SuspendDialogInfo(source);
269         }
270 
271         @Override
272         public SuspendDialogInfo[] newArray(int size) {
273             return new SuspendDialogInfo[size];
274         }
275     };
276 
277     /**
278      * Builder to build a {@link SuspendDialogInfo} object.
279      */
280     public static final class Builder {
281         private int mDialogMessageResId = ID_NULL;
282         private String mDialogMessage;
283         private int mTitleResId = ID_NULL;
284         private int mIconResId = ID_NULL;
285         private int mNeutralButtonTextResId = ID_NULL;
286 
287         /**
288          * Set the resource id of the icon to be used. If not provided, no icon will be shown.
289          *
290          * @param resId The resource id of the icon.
291          * @return this builder object.
292          */
293         @NonNull
setIcon(@rawableRes int resId)294         public Builder setIcon(@DrawableRes int resId) {
295             Preconditions.checkArgument(ResourceId.isValid(resId), "Invalid resource id provided");
296             mIconResId = resId;
297             return this;
298         }
299 
300         /**
301          * Set the resource id of the title text to be displayed. If this is not provided, the
302          * system will use a default title.
303          *
304          * @param resId The resource id of the title.
305          * @return this builder object.
306          */
307         @NonNull
setTitle(@tringRes int resId)308         public Builder setTitle(@StringRes int resId) {
309             Preconditions.checkArgument(ResourceId.isValid(resId), "Invalid resource id provided");
310             mTitleResId = resId;
311             return this;
312         }
313 
314         /**
315          * Set the text to show in the body of the dialog. Ignored if a resource id is set via
316          * {@link #setMessage(int)}.
317          * <p>
318          * The system will use {@link String#format(Locale, String, Object...) String.format} to
319          * insert the suspended app name into the message, so an example format string could be
320          * {@code "The app %1$s is currently suspended"}. This is optional - if the string passed in
321          * {@code message} does not accept an argument, it will be used as is.
322          *
323          * @param message The dialog message.
324          * @return this builder object.
325          * @see #setMessage(int)
326          */
327         @NonNull
setMessage(@onNull String message)328         public Builder setMessage(@NonNull String message) {
329             Preconditions.checkStringNotEmpty(message, "Message cannot be null or empty");
330             mDialogMessage = message;
331             return this;
332         }
333 
334         /**
335          * Set the resource id of the dialog message to be shown. If no dialog message is provided
336          * via either this method or {@link #setMessage(String)}, the system will use a
337          * default message.
338          * <p>
339          * The system will use {@link android.content.res.Resources#getString(int, Object...)
340          * getString} to insert the suspended app name into the message, so an example format string
341          * could be {@code "The app %1$s is currently suspended"}. This is optional - if the string
342          * referred to by {@code resId} does not accept an argument, it will be used as is.
343          *
344          * @param resId The resource id of the dialog message.
345          * @return this builder object.
346          * @see #setMessage(String)
347          */
348         @NonNull
setMessage(@tringRes int resId)349         public Builder setMessage(@StringRes int resId) {
350             Preconditions.checkArgument(ResourceId.isValid(resId), "Invalid resource id provided");
351             mDialogMessageResId = resId;
352             return this;
353         }
354 
355         /**
356          * Set the resource id of text to be shown on the neutral button. Tapping this button starts
357          * the {@link android.content.Intent#ACTION_SHOW_SUSPENDED_APP_DETAILS} activity. If this is
358          * not provided, the system will use a default text.
359          *
360          * @param resId The resource id of the button text
361          * @return this builder object.
362          */
363         @NonNull
setNeutralButtonText(@tringRes int resId)364         public Builder setNeutralButtonText(@StringRes int resId) {
365             Preconditions.checkArgument(ResourceId.isValid(resId), "Invalid resource id provided");
366             mNeutralButtonTextResId = resId;
367             return this;
368         }
369 
370         /**
371          * Build the final object based on given inputs.
372          *
373          * @return The {@link SuspendDialogInfo} object built using this builder.
374          */
375         @NonNull
build()376         public SuspendDialogInfo build() {
377             return new SuspendDialogInfo(this);
378         }
379     }
380 }
381