blob: 21fb2a6927aac828f420485e7855eb6c297e3c98 [file] [log] [blame]
Jeff Sharkey56f03682017-01-23 16:58:02 -07001/*
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
17package android.app;
18
Jeff Sharkeye99566e2018-12-11 17:34:08 -070019import android.annotation.NonNull;
Jeff Sharkey56f03682017-01-23 16:58:02 -070020import android.content.Context;
21import android.os.Bundle;
22import android.os.Parcel;
23import android.os.Parcelable;
24
Jeff Sharkeye99566e2018-12-11 17:34:08 -070025import java.util.Objects;
Jeff Sharkey56f03682017-01-23 16:58:02 -070026
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 Sharkey780861f2017-03-20 14:38:04 -060033 * authentication credentials, or granting access.
34 * <p>
35 * If the receiving app is actively involved with the user, it should present
Jeff Sharkeye99566e2018-12-11 17:34:08 -070036 * the contained recovery details to help the user make forward progress.
Jeff Sharkey56f03682017-01-23 16:58:02 -070037 * <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 Sharkey56f03682017-01-23 16:58:02 -070041 */
42public final class RecoverableSecurityException extends SecurityException implements Parcelable {
43 private static final String TAG = "RecoverableSecurityException";
44
45 private final CharSequence mUserMessage;
Jeff Sharkey72ec4482017-02-12 03:21:25 -070046 private final RemoteAction mUserAction;
Jeff Sharkey56f03682017-01-23 16:58:02 -070047
48 /** {@hide} */
49 public RecoverableSecurityException(Parcel in) {
Jeff Sharkey72ec4482017-02-12 03:21:25 -070050 this(new SecurityException(in.readString()), in.readCharSequence(),
51 RemoteAction.CREATOR.createFromParcel(in));
Jeff Sharkey56f03682017-01-23 16:58:02 -070052 }
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 Sharkey72ec4482017-02-12 03:21:25 -070061 * 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 Sharkey56f03682017-01-23 16:58:02 -070067 * {@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 Lindac516e2017-05-02 13:32:27 -070070 * immediately retry their operation.
Jeff Sharkey56f03682017-01-23 16:58:02 -070071 */
Jeff Sharkeye99566e2018-12-11 17:34:08 -070072 public RecoverableSecurityException(@NonNull Throwable cause, @NonNull CharSequence userMessage,
73 @NonNull RemoteAction userAction) {
Jeff Sharkey56f03682017-01-23 16:58:02 -070074 super(cause.getMessage());
Jeff Sharkeye99566e2018-12-11 17:34:08 -070075 mUserMessage = Objects.requireNonNull(userMessage);
76 mUserAction = Objects.requireNonNull(userAction);
Jeff Sharkey72ec4482017-02-12 03:21:25 -070077 }
78
Jeff Sharkey56f03682017-01-23 16:58:02 -070079 /**
80 * Return short message describing the issue for end user audiences, which
81 * may be shown in a notification or dialog.
82 */
Jeff Sharkeye99566e2018-12-11 17:34:08 -070083 public @NonNull CharSequence getUserMessage() {
Jeff Sharkey56f03682017-01-23 16:58:02 -070084 return mUserMessage;
85 }
86
87 /**
Jeff Sharkey56f03682017-01-23 16:58:02 -070088 * Return primary action that will initiate the recovery.
89 */
Jeff Sharkeye99566e2018-12-11 17:34:08 -070090 public @NonNull RemoteAction getUserAction() {
Jeff Sharkey56f03682017-01-23 16:58:02 -070091 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 Sharkey780861f2017-03-20 14:38:04 -0600107 *
108 * @param channelId the {@link NotificationChannel} to use, which must have
109 * been already created using
110 * {@link NotificationManager#createNotificationChannel}.
Jeff Sharkeye99566e2018-12-11 17:34:08 -0700111 * @hide
Jeff Sharkey56f03682017-01-23 16:58:02 -0700112 */
Jeff Sharkey780861f2017-03-20 14:38:04 -0600113 public void showAsNotification(Context context, String channelId) {
Jeff Sharkey56f03682017-01-23 16:58:02 -0700114 final NotificationManager nm = context.getSystemService(NotificationManager.class);
Jeff Sharkey780861f2017-03-20 14:38:04 -0600115 final Notification.Builder builder = new Notification.Builder(context, channelId)
Jeff Sharkey72ec4482017-02-12 03:21:25 -0700116 .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 Sharkey780861f2017-03-20 14:38:04 -0600121 nm.notify(TAG, mUserAction.getActionIntent().getCreatorUid(), builder.build());
Jeff Sharkey56f03682017-01-23 16:58:02 -0700122 }
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 Sharkeye99566e2018-12-11 17:34:08 -0700137 *
138 * @hide
Jeff Sharkey56f03682017-01-23 16:58:02 -0700139 */
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 Sharkey72ec4482017-02-12 03:21:25 -0700146 final String tag = TAG + "_" + mUserAction.getActionIntent().getCreatorUid();
Jeff Sharkey56f03682017-01-23 16:58:02 -0700147 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 Sharkey780861f2017-03-20 14:38:04 -0600157 /**
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 Sharkey56f03682017-01-23 16:58:02 -0700164 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 Sharkey72ec4482017-02-12 03:21:25 -0700170 .setPositiveButton(e.mUserAction.getTitle(), (dialog, which) -> {
Jeff Sharkey56f03682017-01-23 16:58:02 -0700171 try {
Jeff Sharkey72ec4482017-02-12 03:21:25 -0700172 e.mUserAction.getActionIntent().send();
Jeff Sharkey56f03682017-01-23 16:58:02 -0700173 } 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 Sharkey56f03682017-01-23 16:58:02 -0700190 mUserAction.writeToParcel(dest, flags);
191 }
192
Jeff Sharkey9e8f83d2019-02-28 12:06:45 -0700193 public static final @android.annotation.NonNull Creator<RecoverableSecurityException> CREATOR =
Jeff Sharkey56f03682017-01-23 16:58:02 -0700194 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}