blob: 21fb2a6927aac828f420485e7855eb6c297e3c98 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.app;
import android.annotation.NonNull;
import android.content.Context;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.Objects;
/**
* Specialization of {@link SecurityException} that contains additional
* information about how to involve the end user to recover from the exception.
* <p>
* This exception is only appropriate where there is a concrete action the user
* can take to recover and make forward progress, such as confirming or entering
* authentication credentials, or granting access.
* <p>
* If the receiving app is actively involved with the user, it should present
* the contained recovery details to help the user make forward progress.
* <p class="note">
* Note: legacy code that receives this exception may treat it as a general
* {@link SecurityException}, and thus there is no guarantee that the messages
* contained will be shown to the end user.
*/
public final class RecoverableSecurityException extends SecurityException implements Parcelable {
private static final String TAG = "RecoverableSecurityException";
private final CharSequence mUserMessage;
private final RemoteAction mUserAction;
/** {@hide} */
public RecoverableSecurityException(Parcel in) {
this(new SecurityException(in.readString()), in.readCharSequence(),
RemoteAction.CREATOR.createFromParcel(in));
}
/**
* Create an instance ready to be thrown.
*
* @param cause original cause with details designed for engineering
* audiences.
* @param userMessage short message describing the issue for end user
* audiences, which may be shown in a notification or dialog.
* This should be localized and less than 64 characters. For
* example: <em>PIN required to access Document.pdf</em>
* @param userAction primary action that will initiate the recovery. The
* title should be localized and less than 24 characters. For
* example: <em>Enter PIN</em>. This action must launch an
* activity that is expected to set
* {@link Activity#setResult(int)} before finishing to
* communicate the final status of the recovery. For example,
* apps that observe {@link Activity#RESULT_OK} may choose to
* immediately retry their operation.
*/
public RecoverableSecurityException(@NonNull Throwable cause, @NonNull CharSequence userMessage,
@NonNull RemoteAction userAction) {
super(cause.getMessage());
mUserMessage = Objects.requireNonNull(userMessage);
mUserAction = Objects.requireNonNull(userAction);
}
/**
* Return short message describing the issue for end user audiences, which
* may be shown in a notification or dialog.
*/
public @NonNull CharSequence getUserMessage() {
return mUserMessage;
}
/**
* Return primary action that will initiate the recovery.
*/
public @NonNull RemoteAction getUserAction() {
return mUserAction;
}
/**
* Convenience method that will show a very simple notification populated
* with the details from this exception.
* <p>
* If you want more flexibility over retrying your original operation once
* the user action has finished, consider presenting your own UI that uses
* {@link Activity#startIntentSenderForResult} to launch the
* {@link PendingIntent#getIntentSender()} from {@link #getUserAction()}
* when requested. If the result of that activity is
* {@link Activity#RESULT_OK}, you should consider retrying.
* <p>
* This method will only display the most recent exception from any single
* remote UID; notifications from older exceptions will always be replaced.
*
* @param channelId the {@link NotificationChannel} to use, which must have
* been already created using
* {@link NotificationManager#createNotificationChannel}.
* @hide
*/
public void showAsNotification(Context context, String channelId) {
final NotificationManager nm = context.getSystemService(NotificationManager.class);
final Notification.Builder builder = new Notification.Builder(context, channelId)
.setSmallIcon(com.android.internal.R.drawable.ic_print_error)
.setContentTitle(mUserAction.getTitle())
.setContentText(mUserMessage)
.setContentIntent(mUserAction.getActionIntent())
.setCategory(Notification.CATEGORY_ERROR);
nm.notify(TAG, mUserAction.getActionIntent().getCreatorUid(), builder.build());
}
/**
* Convenience method that will show a very simple dialog populated with the
* details from this exception.
* <p>
* If you want more flexibility over retrying your original operation once
* the user action has finished, consider presenting your own UI that uses
* {@link Activity#startIntentSenderForResult} to launch the
* {@link PendingIntent#getIntentSender()} from {@link #getUserAction()}
* when requested. If the result of that activity is
* {@link Activity#RESULT_OK}, you should consider retrying.
* <p>
* This method will only display the most recent exception from any single
* remote UID; dialogs from older exceptions will always be replaced.
*
* @hide
*/
public void showAsDialog(Activity activity) {
final LocalDialog dialog = new LocalDialog();
final Bundle args = new Bundle();
args.putParcelable(TAG, this);
dialog.setArguments(args);
final String tag = TAG + "_" + mUserAction.getActionIntent().getCreatorUid();
final FragmentManager fm = activity.getFragmentManager();
final FragmentTransaction ft = fm.beginTransaction();
final Fragment old = fm.findFragmentByTag(tag);
if (old != null) {
ft.remove(old);
}
ft.add(dialog, tag);
ft.commitAllowingStateLoss();
}
/**
* Implementation detail for
* {@link RecoverableSecurityException#showAsDialog(Activity)}; needs to
* remain static to be recreated across orientation changes.
*
* @hide
*/
public static class LocalDialog extends DialogFragment {
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final RecoverableSecurityException e = getArguments().getParcelable(TAG);
return new AlertDialog.Builder(getActivity())
.setMessage(e.mUserMessage)
.setPositiveButton(e.mUserAction.getTitle(), (dialog, which) -> {
try {
e.mUserAction.getActionIntent().send();
} catch (PendingIntent.CanceledException ignored) {
}
})
.setNegativeButton(android.R.string.cancel, null)
.create();
}
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(getMessage());
dest.writeCharSequence(mUserMessage);
mUserAction.writeToParcel(dest, flags);
}
public static final @android.annotation.NonNull Creator<RecoverableSecurityException> CREATOR =
new Creator<RecoverableSecurityException>() {
@Override
public RecoverableSecurityException createFromParcel(Parcel source) {
return new RecoverableSecurityException(source);
}
@Override
public RecoverableSecurityException[] newArray(int size) {
return new RecoverableSecurityException[size];
}
};
}