| /* |
| * Copyright (C) 2016 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 com.android.server.autofill; |
| |
| import static android.view.View.AUTO_FILL_FLAG_TYPE_FILL; |
| import static android.view.View.AUTO_FILL_FLAG_TYPE_SAVE; |
| |
| import static com.android.server.autofill.Helper.DEBUG; |
| import static com.android.server.autofill.Helper.bundleToString; |
| |
| import android.app.Activity; |
| import android.app.Notification; |
| import android.app.Notification.Action; |
| import android.app.NotificationManager; |
| import android.app.PendingIntent; |
| import android.app.StatusBarManager; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.os.Binder; |
| import android.os.Bundle; |
| import android.service.autofill.AutoFillService; |
| import android.util.Slog; |
| import android.view.autofill.AutoFillId; |
| import android.view.autofill.Dataset; |
| import android.view.autofill.FillResponse; |
| import android.widget.Toast; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.server.UiThread; |
| |
| import java.util.Arrays; |
| import java.util.List; |
| |
| /** |
| * Handles all auto-fill related UI tasks. |
| */ |
| // TODO(b/33197203): document exactly what once the auto-fill bar is implemented |
| final class AutoFillUI { |
| |
| private static final String TAG = "AutoFillUI"; |
| |
| private final Context mContext; |
| |
| AutoFillUI(Context context, AutoFillManagerService service, Object lock) { |
| mContext = context; |
| mService = service; |
| mLock = lock; |
| |
| setNotificationListener(); |
| } |
| |
| /** |
| * Displays an error message to the user. |
| */ |
| void showError(CharSequence message) { |
| // TODO(b/33197203): proper implementation |
| UiThread.getHandler().runWithScissors(() -> { |
| Toast.makeText(mContext, "AutoFill error: " + message, Toast.LENGTH_LONG).show(); |
| }, 0); |
| } |
| |
| /** |
| * Highlights in the {@link Activity} the fields saved by the service. |
| */ |
| void highlightSavedFields(AutoFillId[] ids) { |
| // TODO(b/33197203): proper implementation (must be handled by activity) |
| UiThread.getHandler().runWithScissors(() -> { |
| Toast.makeText(mContext, "AutoFill: service saved ids " + Arrays.toString(ids), |
| Toast.LENGTH_LONG).show(); |
| }, 0); |
| } |
| |
| /** |
| * Shows the options from a {@link FillResponse} so the user can pick up the proper |
| * {@link Dataset} (when the response has one). |
| */ |
| void showOptions(int userId, int sessionId, FillResponse response) { |
| // TODO(b/33197203): proper implementation |
| // TODO(b/33197203): make sure if removes the callback from cache |
| showOptionsNotification(userId, sessionId, response); |
| } |
| |
| /** |
| * Shows an UI affordance indicating that user action is required before a {@link FillResponse} |
| * can be used. |
| * |
| * <p>It typically replaces the auto-fill bar with a message saying "Press fingerprint or tap to |
| * autofill" or "Tap to autofill", depending on the value of {@code usesFingerprint}. |
| */ |
| void showFillResponseAuthenticationRequest(int userId, int sessionId, boolean usesFingerprint, |
| Bundle extras, int flags) { |
| // TODO(b/33197203): proper implementation |
| showAuthNotification(userId, sessionId, usesFingerprint, extras, flags); |
| } |
| |
| /** |
| * Shows an UI affordance asking indicating that user action is required before a |
| * {@link Dataset} can be used. |
| * |
| * <p>It typically replaces the auto-fill bar with a message saying "Press fingerprint to |
| * autofill". |
| */ |
| void showDatasetFingerprintAuthenticationRequest(Dataset dataset) { |
| if (DEBUG) Slog.d(TAG, "showDatasetAuthenticationRequest(): dataset=" + dataset); |
| |
| // TODO(b/33197203): proper implementation (either pop up a fingerprint dialog or replace |
| // the auto-fill bar with a new message. |
| UiThread.getHandler().runWithScissors(() -> { |
| Toast.makeText(mContext, "AutoFill: press fingerprint to unlock " + dataset.getName(), |
| Toast.LENGTH_LONG).show(); |
| }, 0); |
| } |
| |
| /** |
| * Called by service after the user user the fingerprint sensors to authenticate. |
| */ |
| void dismissFingerprintRequest(int userId, boolean success) { |
| if (DEBUG) Slog.d(TAG, "dismissFingerprintRequest(): ok=" + success); |
| |
| dismissAuthNotification(userId); |
| |
| if (!success) { |
| // TODO(b/33197203): proper implementation (snack bar / i18n string) |
| UiThread.getHandler().runWithScissors(() -> { |
| Toast.makeText(mContext, "AutoFill: fingerprint failed", Toast.LENGTH_LONG).show(); |
| }, 0); |
| } |
| } |
| |
| private AutoFillManagerServiceImpl getServiceLocked(int userId) { |
| final AutoFillManagerServiceImpl service = mService.getServiceForUserLocked(userId); |
| if (service == null) { |
| Slog.w(TAG, "no auto-fill service for user " + userId); |
| } |
| return service; |
| } |
| |
| private void onSaveRequested(int userId, Bundle responseExtras, Bundle datasetExtras) { |
| synchronized (mLock) { |
| final AutoFillManagerServiceImpl service = getServiceLocked(userId); |
| if (service == null) return; |
| |
| final Bundle extras = (responseExtras == null && datasetExtras == null) |
| ? null : new Bundle(); |
| |
| if (responseExtras != null) { |
| if (DEBUG) Slog.d(TAG, "response extras on save notification: " + |
| bundleToString(responseExtras)); |
| extras.putBundle(AutoFillService.EXTRA_RESPONSE_EXTRAS, responseExtras); |
| } |
| if (datasetExtras != null) { |
| if (DEBUG) Slog.d(TAG, "dataset extras on save notificataion: " + |
| bundleToString(datasetExtras)); |
| extras.putBundle(AutoFillService.EXTRA_DATASET_EXTRAS, datasetExtras); |
| } |
| |
| service.requestAutoFill(null, extras, AUTO_FILL_FLAG_TYPE_SAVE); |
| } |
| } |
| |
| private void onDatasetPicked(int userId, Dataset dataset, int sessionId) { |
| synchronized (mLock) { |
| final AutoFillManagerServiceImpl service = getServiceLocked(userId); |
| if (service == null) return; |
| |
| service.autoFillApp(sessionId, dataset); |
| } |
| } |
| |
| private void onSessionDone(int userId, int sessionId) { |
| synchronized (mLock) { |
| final AutoFillManagerServiceImpl service = getServiceLocked(userId); |
| if (service == null) return; |
| |
| service.removeSessionLocked(sessionId); |
| } |
| } |
| |
| private void onResponseAuthenticationRequested(int userId, Bundle extras, int flags) { |
| synchronized (mLock) { |
| final AutoFillManagerServiceImpl service = getServiceLocked(userId); |
| if (service == null) return; |
| |
| service.notifyResponseAuthenticationResult(extras, flags); |
| } |
| } |
| |
| ///////////////////////////////////////////////////////////////////////////////// |
| // TODO(b/33197203): temporary code using a notification to request auto-fill. // |
| // Will be removed once UX decide the right way to present it to the user. // |
| ///////////////////////////////////////////////////////////////////////////////// |
| |
| // TODO(b/33197203): remove from frameworks/base/core/res/AndroidManifest.xml once not used |
| private static final String NOTIFICATION_AUTO_FILL_INTENT = |
| "com.android.internal.autofill.action.REQUEST_AUTOFILL"; |
| |
| // Extras used in the notification intents |
| private static final String EXTRA_USER_ID = "user_id"; |
| private static final String EXTRA_NOTIFICATION_TYPE = "notification_type"; |
| private static final String EXTRA_SESSION_ID = "session_id"; |
| private static final String EXTRA_FILL_RESPONSE = "fill_response"; |
| private static final String EXTRA_DATASET = "dataset"; |
| private static final String EXTRA_AUTH_REQUIRED_EXTRAS = "auth_required_extras"; |
| private static final String EXTRA_FLAGS = "flags"; |
| |
| private static final String TYPE_OPTIONS = "options"; |
| private static final String TYPE_FINISH_SESSION = "finish_session"; |
| private static final String TYPE_PICK_DATASET = "pick_dataset"; |
| private static final String TYPE_SAVE = "save"; |
| private static final String TYPE_AUTH_RESPONSE = "auth_response"; |
| |
| @GuardedBy("mLock") |
| private BroadcastReceiver mNotificationReceiver; |
| @GuardedBy("mLock") |
| private final AutoFillManagerService mService; |
| private final Object mLock; |
| |
| // Hack used to generate unique pending intents |
| static int sResultCode = 0; |
| |
| private void setNotificationListener() { |
| synchronized (mLock) { |
| if (mNotificationReceiver == null) { |
| mNotificationReceiver = new NotificationReceiver(); |
| mContext.registerReceiver(mNotificationReceiver, |
| new IntentFilter(NOTIFICATION_AUTO_FILL_INTENT)); |
| } |
| } |
| } |
| |
| final class NotificationReceiver extends BroadcastReceiver { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| final int userId = intent.getIntExtra(EXTRA_USER_ID, -1); |
| final int sessionId = intent.getIntExtra(EXTRA_SESSION_ID, -1); |
| final String type = intent.getStringExtra(EXTRA_NOTIFICATION_TYPE); |
| if (type == null) { |
| Slog.wtf(TAG, "No extra " + EXTRA_NOTIFICATION_TYPE + " on intent " + intent); |
| return; |
| } |
| final FillResponse response = intent.getParcelableExtra(EXTRA_FILL_RESPONSE); |
| final Dataset dataset = intent.getParcelableExtra(EXTRA_DATASET); |
| final Bundle responseExtras = response == null ? null : response.getExtras(); |
| final Bundle datasetExtras = dataset == null ? null : dataset.getExtras(); |
| final int flags = intent.getIntExtra(EXTRA_FLAGS, 0); |
| |
| if (DEBUG) Slog.d(TAG, "Notification received: type=" + type + ", userId=" + userId |
| + ", sessionId=" + sessionId); |
| synchronized (mLock) { |
| switch (type) { |
| case TYPE_SAVE: |
| onSaveRequested(userId, responseExtras, datasetExtras); |
| break; |
| case TYPE_FINISH_SESSION: |
| onSessionDone(userId, sessionId); |
| break; |
| case TYPE_PICK_DATASET: |
| onDatasetPicked(userId, dataset, sessionId); |
| |
| // Must cancel notification because it might be comming from action |
| if (DEBUG) Slog.d(TAG, "Cancelling notification"); |
| NotificationManager.from(mContext).cancel(TYPE_OPTIONS, userId); |
| |
| break; |
| case TYPE_AUTH_RESPONSE: |
| onResponseAuthenticationRequested(userId, |
| intent.getBundleExtra(EXTRA_AUTH_REQUIRED_EXTRAS), flags); |
| break; |
| default: { |
| Slog.w(TAG, "Unknown notification type: " + type); |
| } |
| } |
| } |
| collapseStatusBar(); |
| } |
| } |
| |
| private static Intent newNotificationIntent(int userId, String type) { |
| final Intent intent = new Intent(NOTIFICATION_AUTO_FILL_INTENT); |
| intent.putExtra(EXTRA_USER_ID, userId); |
| intent.putExtra(EXTRA_NOTIFICATION_TYPE, type); |
| return intent; |
| } |
| |
| private PendingIntent newPickDatasetPI(int userId, int sessionId, FillResponse response, |
| Dataset dataset) { |
| final int resultCode = ++ sResultCode; |
| if (DEBUG) Slog.d(TAG, "newPickDatasetPI: userId=" + userId + ", sessionId=" + sessionId |
| + ", resultCode=" + resultCode); |
| |
| final Intent intent = newNotificationIntent(userId, TYPE_PICK_DATASET); |
| intent.putExtra(EXTRA_SESSION_ID, sessionId); |
| intent.putExtra(EXTRA_FILL_RESPONSE, response); |
| intent.putExtra(EXTRA_DATASET, dataset); |
| return PendingIntent.getBroadcast(mContext, resultCode, intent, |
| PendingIntent.FLAG_ONE_SHOT); |
| } |
| |
| /** |
| * Shows a notification with the results of an auto-fill request, using notications actions |
| * to emulate the auto-fill bar buttons displaying the dataset names. |
| */ |
| private void showOptionsNotification(int userId, int sessionId, FillResponse response) { |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| showOptionsNotificationAsSystem(userId, sessionId, response); |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| private void showOptionsNotificationAsSystem(int userId, int sessionId, |
| FillResponse response) { |
| // Make sure server callback is removed from cache if user cancels the notification. |
| final Intent deleteIntent = newNotificationIntent(userId, TYPE_FINISH_SESSION) |
| .putExtra(EXTRA_SESSION_ID, sessionId); |
| final PendingIntent deletePendingIntent = PendingIntent.getBroadcast(mContext, |
| ++sResultCode, deleteIntent, PendingIntent.FLAG_ONE_SHOT); |
| |
| final String title = "AutoFill Options"; |
| |
| final Notification.Builder notification = newNotificationBuilder() |
| .setOngoing(false) |
| .setDeleteIntent(deletePendingIntent) |
| .setContentTitle(title); |
| |
| boolean autoCancel = true; |
| final String subTitle; |
| final List<Dataset> datasets; |
| final AutoFillId[] savableIds; |
| if (response != null) { |
| datasets = response.getDatasets(); |
| savableIds = response.getSavableIds(); |
| } else { |
| datasets = null; |
| savableIds = null; |
| } |
| boolean showSave = false; |
| if (datasets == null ) { |
| subTitle = "No options to auto-fill this activity."; |
| } else if (datasets.isEmpty()) { |
| if (savableIds.length == 0) { |
| subTitle = "No options to auto-fill this activity."; |
| } else { |
| subTitle = "No options to auto-fill this activity, but provider can save ids:\n" |
| + Arrays.toString(savableIds); |
| showSave = true; |
| } |
| } else { |
| final AutoFillManagerServiceImpl service = mService.getServiceForUserLocked(userId); |
| if (service == null) { |
| subTitle = "No auto-fill service for user " + userId; |
| Slog.w(TAG, subTitle); |
| } else { |
| autoCancel = false; |
| final int size = datasets.size(); |
| subTitle = "There are " + size + " option(s).\n" |
| + "Use the notification action(s) to select the proper one." |
| + "Actions with (F) require fingerprint unlock, and with (P) require" |
| + "provider authentication to unlock"; |
| for (Dataset dataset : datasets) { |
| final StringBuilder name = new StringBuilder(dataset.getName()); |
| if (dataset.isAuthRequired()) { |
| if (dataset.hasCryptoObject()) { |
| name.append("(F)"); |
| } else { |
| name.append("(P)"); |
| } |
| } |
| final PendingIntent pi = newPickDatasetPI(userId, sessionId, response, dataset); |
| notification.addAction(new Action.Builder(null, name, pi).build()); |
| } |
| } |
| } |
| |
| notification.setAutoCancel(autoCancel); |
| notification.setStyle(new Notification.BigTextStyle().bigText(subTitle)); |
| |
| NotificationManager.from(mContext).notify(TYPE_OPTIONS, userId, notification.build()); |
| |
| if (showSave) { |
| showSaveNotification(userId, response, null); |
| } |
| } |
| |
| void showSaveNotification(int userId, FillResponse response, Dataset dataset) { |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| showSaveNotificationAsSystem(userId, response, dataset); |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| private void showSaveNotificationAsSystem(int userId, FillResponse response, Dataset dataset) { |
| final Intent saveIntent = newNotificationIntent(userId, TYPE_SAVE); |
| if (response != null) { |
| saveIntent.putExtra(EXTRA_FILL_RESPONSE, response); |
| } |
| if (dataset != null) { |
| saveIntent.putExtra(EXTRA_DATASET, dataset); |
| } |
| final PendingIntent savePendingIntent = PendingIntent.getBroadcast(mContext, |
| ++sResultCode, saveIntent, PendingIntent.FLAG_ONE_SHOT); |
| |
| final String title = "AutoFill Save"; |
| // Response is not set after fillign an authenticated dataset... |
| final String subTitle = response == null |
| ? "Tap notification to ask provider to save fields." |
| : "Tap notification to ask provider to save fields: \n" |
| + Arrays.toString(response.getSavableIds()); |
| |
| final Notification notification = newNotificationBuilder() |
| .setAutoCancel(true) |
| .setOngoing(false) |
| .setContentTitle(title) |
| .setContentIntent(savePendingIntent) |
| .setStyle(new Notification.BigTextStyle().bigText(subTitle)) |
| .build(); |
| NotificationManager.from(mContext).notify(TYPE_SAVE, userId, notification); |
| } |
| |
| private void showAuthNotification(int userId, int sessionId, boolean usesFingerprint, |
| Bundle extras, int flags) { |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| showAuthNotificationAsSystem(userId, sessionId, usesFingerprint, extras, flags); |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| private void showAuthNotificationAsSystem(int userId, int sessionId, |
| boolean usesFingerprint, Bundle extras, int flags) { |
| final String title = "AutoFill Authentication"; |
| final StringBuilder subTitle = new StringBuilder("Provider require user authentication.\n"); |
| |
| final Intent authIntent = newNotificationIntent(userId, TYPE_AUTH_RESPONSE) |
| .putExtra(EXTRA_SESSION_ID, sessionId); |
| if (extras != null) { |
| authIntent.putExtra(EXTRA_AUTH_REQUIRED_EXTRAS, extras); |
| } |
| if (flags != 0) { |
| authIntent.putExtra(EXTRA_FLAGS, flags); |
| } |
| final PendingIntent authPendingIntent = PendingIntent.getBroadcast(mContext, ++sResultCode, |
| authIntent, PendingIntent.FLAG_ONE_SHOT); |
| |
| if (usesFingerprint) { |
| subTitle.append("But kindly accepts your fingerprint instead" |
| + "\n(tap fingerprint sensor to trigger it)"); |
| |
| } else { |
| subTitle.append("Tap notification to launch its authentication UI."); |
| } |
| |
| final Notification.Builder notification = newNotificationBuilder() |
| .setAutoCancel(true) |
| .setOngoing(false) |
| .setContentTitle(title) |
| .setStyle(new Notification.BigTextStyle().bigText(subTitle.toString())); |
| if (authPendingIntent != null) { |
| notification.setContentIntent(authPendingIntent); |
| } |
| NotificationManager.from(mContext).notify(TYPE_AUTH_RESPONSE, userId, notification.build()); |
| } |
| |
| private void dismissAuthNotification(int userId) { |
| NotificationManager.from(mContext).cancel(TYPE_AUTH_RESPONSE, userId); |
| } |
| |
| private Notification.Builder newNotificationBuilder() { |
| return new Notification.Builder(mContext) |
| .setCategory(Notification.CATEGORY_SYSTEM) |
| .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb) |
| .setLocalOnly(true) |
| .setColor(mContext.getColor( |
| com.android.internal.R.color.system_notification_accent_color)); |
| } |
| |
| private void collapseStatusBar() { |
| final StatusBarManager sbm = (StatusBarManager) mContext.getSystemService("statusbar"); |
| sbm.collapsePanels(); |
| } |
| ///////////////////////////////////////// |
| // End of temporary notification code. // |
| ///////////////////////////////////////// |
| } |