1 /* 2 * Copyright (C) 2017 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.app; 18 19 import android.annotation.NonNull; 20 import android.content.Context; 21 import android.os.Bundle; 22 import android.os.Parcel; 23 import android.os.Parcelable; 24 25 import java.util.Objects; 26 27 /** 28 * Specialization of {@link SecurityException} that contains additional 29 * information about how to involve the end user to recover from the exception. 30 * <p> 31 * This exception is only appropriate where there is a concrete action the user 32 * can take to recover and make forward progress, such as confirming or entering 33 * authentication credentials, or granting access. 34 * <p> 35 * If the receiving app is actively involved with the user, it should present 36 * the contained recovery details to help the user make forward progress. 37 * <p class="note"> 38 * Note: legacy code that receives this exception may treat it as a general 39 * {@link SecurityException}, and thus there is no guarantee that the messages 40 * contained will be shown to the end user. 41 */ 42 public final class RecoverableSecurityException extends SecurityException implements Parcelable { 43 private static final String TAG = "RecoverableSecurityException"; 44 45 private final CharSequence mUserMessage; 46 private final RemoteAction mUserAction; 47 48 /** {@hide} */ RecoverableSecurityException(Parcel in)49 public RecoverableSecurityException(Parcel in) { 50 this(new SecurityException(in.readString()), in.readCharSequence(), 51 RemoteAction.CREATOR.createFromParcel(in)); 52 } 53 54 /** 55 * Create an instance ready to be thrown. 56 * 57 * @param cause original cause with details designed for engineering 58 * audiences. 59 * @param userMessage short message describing the issue for end user 60 * audiences, which may be shown in a notification or dialog. 61 * This should be localized and less than 64 characters. For 62 * example: <em>PIN required to access Document.pdf</em> 63 * @param userAction primary action that will initiate the recovery. The 64 * title should be localized and less than 24 characters. For 65 * example: <em>Enter PIN</em>. This action must launch an 66 * activity that is expected to set 67 * {@link Activity#setResult(int)} before finishing to 68 * communicate the final status of the recovery. For example, 69 * apps that observe {@link Activity#RESULT_OK} may choose to 70 * immediately retry their operation. 71 */ RecoverableSecurityException(@onNull Throwable cause, @NonNull CharSequence userMessage, @NonNull RemoteAction userAction)72 public RecoverableSecurityException(@NonNull Throwable cause, @NonNull CharSequence userMessage, 73 @NonNull RemoteAction userAction) { 74 super(cause.getMessage()); 75 mUserMessage = Objects.requireNonNull(userMessage); 76 mUserAction = Objects.requireNonNull(userAction); 77 } 78 79 /** 80 * Return short message describing the issue for end user audiences, which 81 * may be shown in a notification or dialog. 82 */ getUserMessage()83 public @NonNull CharSequence getUserMessage() { 84 return mUserMessage; 85 } 86 87 /** 88 * Return primary action that will initiate the recovery. 89 */ getUserAction()90 public @NonNull RemoteAction getUserAction() { 91 return mUserAction; 92 } 93 94 /** 95 * Convenience method that will show a very simple notification populated 96 * with the details from this exception. 97 * <p> 98 * If you want more flexibility over retrying your original operation once 99 * the user action has finished, consider presenting your own UI that uses 100 * {@link Activity#startIntentSenderForResult} to launch the 101 * {@link PendingIntent#getIntentSender()} from {@link #getUserAction()} 102 * when requested. If the result of that activity is 103 * {@link Activity#RESULT_OK}, you should consider retrying. 104 * <p> 105 * This method will only display the most recent exception from any single 106 * remote UID; notifications from older exceptions will always be replaced. 107 * 108 * @param channelId the {@link NotificationChannel} to use, which must have 109 * been already created using 110 * {@link NotificationManager#createNotificationChannel}. 111 * @hide 112 */ showAsNotification(Context context, String channelId)113 public void showAsNotification(Context context, String channelId) { 114 final NotificationManager nm = context.getSystemService(NotificationManager.class); 115 final Notification.Builder builder = new Notification.Builder(context, channelId) 116 .setSmallIcon(com.android.internal.R.drawable.ic_print_error) 117 .setContentTitle(mUserAction.getTitle()) 118 .setContentText(mUserMessage) 119 .setContentIntent(mUserAction.getActionIntent()) 120 .setCategory(Notification.CATEGORY_ERROR); 121 nm.notify(TAG, mUserAction.getActionIntent().getCreatorUid(), builder.build()); 122 } 123 124 /** 125 * Convenience method that will show a very simple dialog populated with the 126 * details from this exception. 127 * <p> 128 * If you want more flexibility over retrying your original operation once 129 * the user action has finished, consider presenting your own UI that uses 130 * {@link Activity#startIntentSenderForResult} to launch the 131 * {@link PendingIntent#getIntentSender()} from {@link #getUserAction()} 132 * when requested. If the result of that activity is 133 * {@link Activity#RESULT_OK}, you should consider retrying. 134 * <p> 135 * This method will only display the most recent exception from any single 136 * remote UID; dialogs from older exceptions will always be replaced. 137 * 138 * @hide 139 */ showAsDialog(Activity activity)140 public void showAsDialog(Activity activity) { 141 final LocalDialog dialog = new LocalDialog(); 142 final Bundle args = new Bundle(); 143 args.putParcelable(TAG, this); 144 dialog.setArguments(args); 145 146 final String tag = TAG + "_" + mUserAction.getActionIntent().getCreatorUid(); 147 final FragmentManager fm = activity.getFragmentManager(); 148 final FragmentTransaction ft = fm.beginTransaction(); 149 final Fragment old = fm.findFragmentByTag(tag); 150 if (old != null) { 151 ft.remove(old); 152 } 153 ft.add(dialog, tag); 154 ft.commitAllowingStateLoss(); 155 } 156 157 /** 158 * Implementation detail for 159 * {@link RecoverableSecurityException#showAsDialog(Activity)}; needs to 160 * remain static to be recreated across orientation changes. 161 * 162 * @hide 163 */ 164 public static class LocalDialog extends DialogFragment { 165 @Override onCreateDialog(Bundle savedInstanceState)166 public Dialog onCreateDialog(Bundle savedInstanceState) { 167 final RecoverableSecurityException e = getArguments().getParcelable(TAG); 168 return new AlertDialog.Builder(getActivity()) 169 .setMessage(e.mUserMessage) 170 .setPositiveButton(e.mUserAction.getTitle(), (dialog, which) -> { 171 try { 172 e.mUserAction.getActionIntent().send(); 173 } catch (PendingIntent.CanceledException ignored) { 174 } 175 }) 176 .setNegativeButton(android.R.string.cancel, null) 177 .create(); 178 } 179 } 180 181 @Override 182 public int describeContents() { 183 return 0; 184 } 185 186 @Override 187 public void writeToParcel(Parcel dest, int flags) { 188 dest.writeString(getMessage()); 189 dest.writeCharSequence(mUserMessage); 190 mUserAction.writeToParcel(dest, flags); 191 } 192 193 public static final @android.annotation.NonNull Creator<RecoverableSecurityException> CREATOR = 194 new Creator<RecoverableSecurityException>() { 195 @Override 196 public RecoverableSecurityException createFromParcel(Parcel source) { 197 return new RecoverableSecurityException(source); 198 } 199 200 @Override 201 public RecoverableSecurityException[] newArray(int size) { 202 return new RecoverableSecurityException[size]; 203 } 204 }; 205 } 206