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