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