Jeff Sharkey | 56f0368 | 2017-01-23 16:58:02 -0700 | [diff] [blame] | 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 | |
Jeff Sharkey | e99566e | 2018-12-11 17:34:08 -0700 | [diff] [blame] | 19 | import android.annotation.NonNull; |
Jeff Sharkey | 56f0368 | 2017-01-23 16:58:02 -0700 | [diff] [blame] | 20 | import android.content.Context; |
| 21 | import android.os.Bundle; |
| 22 | import android.os.Parcel; |
| 23 | import android.os.Parcelable; |
| 24 | |
Jeff Sharkey | e99566e | 2018-12-11 17:34:08 -0700 | [diff] [blame] | 25 | import java.util.Objects; |
Jeff Sharkey | 56f0368 | 2017-01-23 16:58:02 -0700 | [diff] [blame] | 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 |
Jeff Sharkey | 780861f | 2017-03-20 14:38:04 -0600 | [diff] [blame] | 33 | * authentication credentials, or granting access. |
| 34 | * <p> |
| 35 | * If the receiving app is actively involved with the user, it should present |
Jeff Sharkey | e99566e | 2018-12-11 17:34:08 -0700 | [diff] [blame] | 36 | * the contained recovery details to help the user make forward progress. |
Jeff Sharkey | 56f0368 | 2017-01-23 16:58:02 -0700 | [diff] [blame] | 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. |
Jeff Sharkey | 56f0368 | 2017-01-23 16:58:02 -0700 | [diff] [blame] | 41 | */ |
| 42 | public final class RecoverableSecurityException extends SecurityException implements Parcelable { |
| 43 | private static final String TAG = "RecoverableSecurityException"; |
| 44 | |
| 45 | private final CharSequence mUserMessage; |
Jeff Sharkey | 72ec448 | 2017-02-12 03:21:25 -0700 | [diff] [blame] | 46 | private final RemoteAction mUserAction; |
Jeff Sharkey | 56f0368 | 2017-01-23 16:58:02 -0700 | [diff] [blame] | 47 | |
| 48 | /** {@hide} */ |
| 49 | public RecoverableSecurityException(Parcel in) { |
Jeff Sharkey | 72ec448 | 2017-02-12 03:21:25 -0700 | [diff] [blame] | 50 | this(new SecurityException(in.readString()), in.readCharSequence(), |
| 51 | RemoteAction.CREATOR.createFromParcel(in)); |
Jeff Sharkey | 56f0368 | 2017-01-23 16:58:02 -0700 | [diff] [blame] | 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. |
Jeff Sharkey | 72ec448 | 2017-02-12 03:21:25 -0700 | [diff] [blame] | 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 |
Jeff Sharkey | 56f0368 | 2017-01-23 16:58:02 -0700 | [diff] [blame] | 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 |
Ben Lin | dac516e | 2017-05-02 13:32:27 -0700 | [diff] [blame] | 70 | * immediately retry their operation. |
Jeff Sharkey | 56f0368 | 2017-01-23 16:58:02 -0700 | [diff] [blame] | 71 | */ |
Jeff Sharkey | e99566e | 2018-12-11 17:34:08 -0700 | [diff] [blame] | 72 | public RecoverableSecurityException(@NonNull Throwable cause, @NonNull CharSequence userMessage, |
| 73 | @NonNull RemoteAction userAction) { |
Jeff Sharkey | 56f0368 | 2017-01-23 16:58:02 -0700 | [diff] [blame] | 74 | super(cause.getMessage()); |
Jeff Sharkey | e99566e | 2018-12-11 17:34:08 -0700 | [diff] [blame] | 75 | mUserMessage = Objects.requireNonNull(userMessage); |
| 76 | mUserAction = Objects.requireNonNull(userAction); |
Jeff Sharkey | 72ec448 | 2017-02-12 03:21:25 -0700 | [diff] [blame] | 77 | } |
| 78 | |
Jeff Sharkey | 56f0368 | 2017-01-23 16:58:02 -0700 | [diff] [blame] | 79 | /** |
| 80 | * Return short message describing the issue for end user audiences, which |
| 81 | * may be shown in a notification or dialog. |
| 82 | */ |
Jeff Sharkey | e99566e | 2018-12-11 17:34:08 -0700 | [diff] [blame] | 83 | public @NonNull CharSequence getUserMessage() { |
Jeff Sharkey | 56f0368 | 2017-01-23 16:58:02 -0700 | [diff] [blame] | 84 | return mUserMessage; |
| 85 | } |
| 86 | |
| 87 | /** |
Jeff Sharkey | 56f0368 | 2017-01-23 16:58:02 -0700 | [diff] [blame] | 88 | * Return primary action that will initiate the recovery. |
| 89 | */ |
Jeff Sharkey | e99566e | 2018-12-11 17:34:08 -0700 | [diff] [blame] | 90 | public @NonNull RemoteAction getUserAction() { |
Jeff Sharkey | 56f0368 | 2017-01-23 16:58:02 -0700 | [diff] [blame] | 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. |
Jeff Sharkey | 780861f | 2017-03-20 14:38:04 -0600 | [diff] [blame] | 107 | * |
| 108 | * @param channelId the {@link NotificationChannel} to use, which must have |
| 109 | * been already created using |
| 110 | * {@link NotificationManager#createNotificationChannel}. |
Jeff Sharkey | e99566e | 2018-12-11 17:34:08 -0700 | [diff] [blame] | 111 | * @hide |
Jeff Sharkey | 56f0368 | 2017-01-23 16:58:02 -0700 | [diff] [blame] | 112 | */ |
Jeff Sharkey | 780861f | 2017-03-20 14:38:04 -0600 | [diff] [blame] | 113 | public void showAsNotification(Context context, String channelId) { |
Jeff Sharkey | 56f0368 | 2017-01-23 16:58:02 -0700 | [diff] [blame] | 114 | final NotificationManager nm = context.getSystemService(NotificationManager.class); |
Jeff Sharkey | 780861f | 2017-03-20 14:38:04 -0600 | [diff] [blame] | 115 | final Notification.Builder builder = new Notification.Builder(context, channelId) |
Jeff Sharkey | 72ec448 | 2017-02-12 03:21:25 -0700 | [diff] [blame] | 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); |
Jeff Sharkey | 780861f | 2017-03-20 14:38:04 -0600 | [diff] [blame] | 121 | nm.notify(TAG, mUserAction.getActionIntent().getCreatorUid(), builder.build()); |
Jeff Sharkey | 56f0368 | 2017-01-23 16:58:02 -0700 | [diff] [blame] | 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. |
Jeff Sharkey | e99566e | 2018-12-11 17:34:08 -0700 | [diff] [blame] | 137 | * |
| 138 | * @hide |
Jeff Sharkey | 56f0368 | 2017-01-23 16:58:02 -0700 | [diff] [blame] | 139 | */ |
| 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 | |
Jeff Sharkey | 72ec448 | 2017-02-12 03:21:25 -0700 | [diff] [blame] | 146 | final String tag = TAG + "_" + mUserAction.getActionIntent().getCreatorUid(); |
Jeff Sharkey | 56f0368 | 2017-01-23 16:58:02 -0700 | [diff] [blame] | 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 | |
Jeff Sharkey | 780861f | 2017-03-20 14:38:04 -0600 | [diff] [blame] | 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 | */ |
Jeff Sharkey | 56f0368 | 2017-01-23 16:58:02 -0700 | [diff] [blame] | 164 | public static class LocalDialog extends DialogFragment { |
| 165 | @Override |
| 166 | public Dialog onCreateDialog(Bundle savedInstanceState) { |
| 167 | final RecoverableSecurityException e = getArguments().getParcelable(TAG); |
| 168 | return new AlertDialog.Builder(getActivity()) |
| 169 | .setMessage(e.mUserMessage) |
Jeff Sharkey | 72ec448 | 2017-02-12 03:21:25 -0700 | [diff] [blame] | 170 | .setPositiveButton(e.mUserAction.getTitle(), (dialog, which) -> { |
Jeff Sharkey | 56f0368 | 2017-01-23 16:58:02 -0700 | [diff] [blame] | 171 | try { |
Jeff Sharkey | 72ec448 | 2017-02-12 03:21:25 -0700 | [diff] [blame] | 172 | e.mUserAction.getActionIntent().send(); |
Jeff Sharkey | 56f0368 | 2017-01-23 16:58:02 -0700 | [diff] [blame] | 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); |
Jeff Sharkey | 56f0368 | 2017-01-23 16:58:02 -0700 | [diff] [blame] | 190 | mUserAction.writeToParcel(dest, flags); |
| 191 | } |
| 192 | |
| 193 | public static final 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 | } |