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