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