Merge "Use floating bar to show authentication sign-in request."
This commit is contained in:
committed by
Android (Google) Code Review
commit
f4de7400fd
@@ -360,13 +360,19 @@ final class AutoFillManagerServiceImpl {
|
||||
|
||||
final AutoFillId mId;
|
||||
private final Listener mListener;
|
||||
// // TODO(b/33197203): does it really need a reference to the session's response?
|
||||
private FillResponse mResponse;
|
||||
// TODO(b/33197203): would not need a reference to response if it was an inner class of
|
||||
// Session...
|
||||
FillResponse mResponse;
|
||||
|
||||
Intent mAuthIntent;
|
||||
|
||||
ComponentName mServiceComponent;
|
||||
private AutoFillValue mAutoFillValue;
|
||||
private Rect mBounds;
|
||||
|
||||
private boolean mValueUpdated;
|
||||
|
||||
|
||||
ViewState(AutoFillId id, Listener listener) {
|
||||
mId = id;
|
||||
mListener = listener;
|
||||
@@ -380,6 +386,16 @@ final class AutoFillManagerServiceImpl {
|
||||
maybeCallOnFillReady();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used when a {@link FillResponse} requires authentication to be unlocked.
|
||||
*/
|
||||
void setResponse(FillResponse response, ComponentName serviceComponent, Intent authIntent) {
|
||||
mAuthIntent = authIntent;
|
||||
mServiceComponent = serviceComponent;
|
||||
setResponse(response);
|
||||
}
|
||||
|
||||
|
||||
// TODO(b/33197203): need to refactor / rename / document this method to make it clear that
|
||||
// it can change the value and update the UI; similarly, should replace code that
|
||||
// directly sets mAutoFilLValue to use encapsulation.
|
||||
@@ -417,8 +433,9 @@ final class AutoFillManagerServiceImpl {
|
||||
pw.print(prefix); pw.print("value:" ); pw.println(mAutoFillValue);
|
||||
pw.print(prefix); pw.print("updated:" ); pw.println(mValueUpdated);
|
||||
pw.print(prefix); pw.print("bounds:" ); pw.println(mBounds);
|
||||
pw.print(prefix); pw.print("authIntent:" ); pw.println(mAuthIntent);
|
||||
pw.print(prefix); pw.print("serviceComponent:" ); pw.println(mServiceComponent);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -451,7 +468,7 @@ final class AutoFillManagerServiceImpl {
|
||||
private final IAutoFillAppCallback mAppCallback;
|
||||
|
||||
@GuardedBy("mLock")
|
||||
RemoteFillService mRemoteFillService;
|
||||
private RemoteFillService mRemoteFillService;
|
||||
|
||||
// TODO(b/33197203): Get a response per view instead of per activity.
|
||||
@GuardedBy("mLock")
|
||||
@@ -731,7 +748,6 @@ final class AutoFillManagerServiceImpl {
|
||||
filterText = text.toString();
|
||||
}
|
||||
}
|
||||
|
||||
getUiForShowing().showFillUi(mActivityToken, viewState, response.getDatasets(),
|
||||
bounds, filterText);
|
||||
}
|
||||
@@ -742,6 +758,12 @@ final class AutoFillManagerServiceImpl {
|
||||
|
||||
// TODO(b/33197203): add MetricsLogger calls
|
||||
|
||||
if (mCurrentViewState == null) {
|
||||
// TODO(b/33197203): temporary sanity check; should never happen
|
||||
Slog.w(TAG, "processResponseLocked(): mCurrentResponse is null");
|
||||
return;
|
||||
}
|
||||
|
||||
mCurrentResponse = response;
|
||||
|
||||
if (mCurrentResponse.getAuthentication() != null) {
|
||||
@@ -762,14 +784,13 @@ final class AutoFillManagerServiceImpl {
|
||||
|
||||
@Override
|
||||
public void onFailure(CharSequence message) {
|
||||
// TODO(b/33197203): call enableSessionLocked(false)
|
||||
getUiForShowing().showError(message);
|
||||
removeSelf();
|
||||
}
|
||||
}));
|
||||
|
||||
getUiForShowing().showFillResponseAuthRequest(
|
||||
mCurrentResponse.getAuthentication(), fillInIntent);
|
||||
return;
|
||||
mCurrentViewState.setResponse(mCurrentResponse, mComponent, fillInIntent);
|
||||
return;
|
||||
}
|
||||
|
||||
final ArraySet<AutoFillId> savableIds = mCurrentResponse.getSavableIds();
|
||||
@@ -783,9 +804,7 @@ final class AutoFillManagerServiceImpl {
|
||||
}
|
||||
|
||||
// TODO(b/33197203): Consider using mCurrentResponse, depends on partitioning design
|
||||
if (mCurrentViewState != null) {
|
||||
mCurrentViewState.setResponse(mCurrentResponse);
|
||||
}
|
||||
mCurrentViewState.setResponse(mCurrentResponse);
|
||||
}
|
||||
|
||||
void autoFill(Dataset dataset) {
|
||||
@@ -817,6 +836,7 @@ final class AutoFillManagerServiceImpl {
|
||||
|
||||
@Override
|
||||
public void onFailure(CharSequence message) {
|
||||
// TODO(b/33197203): call enableSessionLocked(false)
|
||||
getUiForShowing().showError(message);
|
||||
removeSelf();
|
||||
}
|
||||
@@ -828,7 +848,7 @@ final class AutoFillManagerServiceImpl {
|
||||
|
||||
private Intent createAuthFillInIntent(String itemId, AssistStructure structure,
|
||||
Bundle extras, FillCallback fillCallback) {
|
||||
Intent fillInIntent = new Intent();
|
||||
final Intent fillInIntent = new Intent();
|
||||
fillInIntent.putExtra(Intent.EXTRA_AUTO_FILL_ITEM_ID, itemId);
|
||||
fillInIntent.putExtra(Intent.EXTRA_AUTO_FILL_ASSIST_STRUCTURE, structure);
|
||||
fillInIntent.putExtra(Intent.EXTRA_AUTO_FILL_EXTRAS, extras);
|
||||
|
||||
@@ -18,29 +18,22 @@ package com.android.server.autofill;
|
||||
import static com.android.server.autofill.Helper.DEBUG;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.app.Notification;
|
||||
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.content.IntentSender;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.util.ArraySet;
|
||||
import android.os.Looper;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.ArraySet;
|
||||
import android.util.Slog;
|
||||
import android.view.autofill.Dataset;
|
||||
import android.view.autofill.FillResponse;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.view.WindowManager.LayoutParams;
|
||||
import android.view.autofill.Dataset;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.android.internal.os.HandlerCaller;
|
||||
@@ -58,19 +51,15 @@ final class AutoFillUI {
|
||||
private static final long SNACK_BAR_LIFETIME_MS = 30 * DateUtils.SECOND_IN_MILLIS;
|
||||
private static final int MSG_HIDE_SNACK_BAR = 1;
|
||||
|
||||
private static final String EXTRA_AUTH_INTENT_SENDER =
|
||||
"com.android.server.autofill.extra.AUTH_INTENT_SENDER";
|
||||
private static final String EXTRA_AUTH_FILL_IN_INTENT =
|
||||
"com.android.server.autofill.extra.AUTH_FILL_IN_INTENT";
|
||||
|
||||
private final Context mContext;
|
||||
private final WindowManager mWm;
|
||||
|
||||
// TODO(b/33197203) Fix locking - some state requires lock and some not - requires refactoring
|
||||
private final Object mLock = new Object();
|
||||
|
||||
// Fill UI variables
|
||||
private AnchoredWindow mFillWindow;
|
||||
private DatasetPicker mFillView;
|
||||
private View mFillView;
|
||||
private ViewState mViewState;
|
||||
|
||||
private AutoFillUiCallback mCallback;
|
||||
@@ -158,62 +147,85 @@ final class AutoFillUI {
|
||||
|
||||
UiThread.getHandler().runWithScissors(() -> {
|
||||
hideSnackbarUiThread();
|
||||
hideFillResponseAuthUiUiThread();
|
||||
}, 0);
|
||||
|
||||
if (datasets == null) {
|
||||
if (datasets == null && viewState.mAuthIntent == null) {
|
||||
// TODO(b/33197203): shouldn't be called, but keeping the WTF for a while just to be
|
||||
// safe, otherwise it would crash system server...
|
||||
Slog.wtf(TAG, "showFillUI(): no dataset");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO(b/33197203): should not display UI after we launched an authentication intent, since
|
||||
// we have no warranty the provider will call onFailure() if the authentication failed or
|
||||
// user dismissed the auth window
|
||||
// because if the service does not handle calling the callback,
|
||||
|
||||
UiThread.getHandler().runWithScissors(() -> {
|
||||
// The dataset picker is only shown when authentication is not required...
|
||||
DatasetPicker datasetPicker = null;
|
||||
|
||||
if (mViewState == null || !mViewState.mId.equals(viewState.mId)) {
|
||||
hideFillUiUiThread();
|
||||
|
||||
mViewState = viewState;
|
||||
|
||||
mFillView = new DatasetPicker(mContext, datasets,
|
||||
(dataset) -> {
|
||||
final AutoFillUiCallback callback;
|
||||
synchronized (mLock) {
|
||||
callback = mCallback;
|
||||
}
|
||||
if (callback != null) {
|
||||
callback.fill(dataset);
|
||||
} else {
|
||||
Slog.w(TAG, "null callback on showFillUi() for " + viewState.mId);
|
||||
}
|
||||
hideFillUi();
|
||||
});
|
||||
if (viewState.mAuthIntent != null) {
|
||||
final String packageName = viewState.mServiceComponent.getPackageName();
|
||||
CharSequence serviceName = null;
|
||||
try {
|
||||
final PackageManager pm = mContext.getPackageManager();
|
||||
final ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
|
||||
serviceName = pm.getApplicationLabel(info);
|
||||
} catch (Exception e) {
|
||||
Slog.w(TAG, "Could not get label for " + packageName + ": " + e);
|
||||
serviceName = packageName;
|
||||
}
|
||||
|
||||
mFillView = new SignInPrompt(mContext, serviceName, (e) -> {
|
||||
final IntentSender intentSender = viewState.mResponse.getAuthentication();
|
||||
final AutoFillUiCallback callback;
|
||||
final Intent authIntent;
|
||||
synchronized (mLock) {
|
||||
callback = mCallback;
|
||||
authIntent = viewState.mAuthIntent;
|
||||
// Must reset the authentication intent so UI display the datasets after
|
||||
// the user authenticated.
|
||||
viewState.mAuthIntent = null;
|
||||
}
|
||||
if (callback != null) {
|
||||
callback.authenticate(intentSender, authIntent);
|
||||
} else {
|
||||
// TODO(b/33197203): need to figure out why it's null sometimes
|
||||
Slog.w(TAG, "no callback on showFillUi().auth for " + viewState.mId);
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
mFillView = datasetPicker = new DatasetPicker(mContext, datasets,
|
||||
(dataset) -> {
|
||||
final AutoFillUiCallback callback;
|
||||
synchronized (mLock) {
|
||||
callback = mCallback;
|
||||
}
|
||||
if (callback != null) {
|
||||
callback.fill(dataset);
|
||||
} else {
|
||||
// TODO(b/33197203): need to figure out why it's null sometimes
|
||||
Slog.w(TAG, "no callback on showFillUi() for " + viewState.mId);
|
||||
}
|
||||
hideFillUiUiThread();
|
||||
});
|
||||
}
|
||||
mFillWindow = new AnchoredWindow(mWm, appToken, mFillView);
|
||||
|
||||
if (DEBUG) Slog.d(TAG, "showFillUi(): view changed");
|
||||
if (DEBUG) Slog.d(TAG, "showFillUi(): view changed for: " + viewState.mId);
|
||||
}
|
||||
|
||||
if (DEBUG) Slog.d(TAG, "showFillUi(): bounds=" + bounds + ", filterText=" + filterText);
|
||||
mFillView.update(filterText);
|
||||
if (datasetPicker != null) {
|
||||
datasetPicker.update(filterText);
|
||||
}
|
||||
mFillWindow.show(bounds);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 showFillResponseAuthRequest(IntentSender intent, Intent fillInIntent) {
|
||||
if (!hasCallback()) {
|
||||
return;
|
||||
}
|
||||
hideAll();
|
||||
UiThread.getHandler().runWithScissors(() -> {
|
||||
// TODO(b/33197203): proper implementation
|
||||
showFillResponseAuthUiUiThread(intent, fillInIntent);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
@@ -251,14 +263,12 @@ final class AutoFillUI {
|
||||
UiThread.getHandler().runWithScissors(() -> {
|
||||
hideSnackbarUiThread();
|
||||
hideFillUiUiThread();
|
||||
hideFillResponseAuthUiUiThread();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
void dump(PrintWriter pw) {
|
||||
pw.println("AufoFill UI");
|
||||
final String prefix = " ";
|
||||
pw.print(prefix); pw.print("sResultCode: "); pw.println(sResultCode);
|
||||
pw.print(prefix); pw.print("mActivityToken: "); pw.println(mActivityToken);
|
||||
pw.print(prefix); pw.print("mSnackBar: "); pw.println(mSnackbar);
|
||||
pw.print(prefix); pw.print("mViewState: "); pw.println(mViewState);
|
||||
@@ -310,104 +320,4 @@ final class AutoFillUI {
|
||||
void authenticate(IntentSender intent, Intent fillInIntent);
|
||||
void fill(Dataset dataset);
|
||||
void save();
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// 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";
|
||||
|
||||
private BroadcastReceiver mNotificationReceiver;
|
||||
private final Object mLock = new Object();
|
||||
|
||||
// Hack used to generate unique pending intents
|
||||
static int sResultCode = 0;
|
||||
|
||||
private void ensureNotificationListener() {
|
||||
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 AutoFillUiCallback callback;
|
||||
synchronized (mLock) {
|
||||
callback = mCallback;
|
||||
}
|
||||
if (callback != null) {
|
||||
IntentSender intentSender = intent.getParcelableExtra(EXTRA_AUTH_INTENT_SENDER);
|
||||
Intent fillInIntent = intent.getParcelableExtra(EXTRA_AUTH_FILL_IN_INTENT);
|
||||
callback.authenticate(intentSender, fillInIntent);
|
||||
}
|
||||
collapseStatusBar();
|
||||
}
|
||||
}
|
||||
|
||||
@android.annotation.UiThread
|
||||
private void showFillResponseAuthUiUiThread(IntentSender intent, Intent fillInIntent) {
|
||||
final String title = "AutoFill Authentication";
|
||||
final StringBuilder subTitle = new StringBuilder("Provider require user authentication.\n");
|
||||
|
||||
final Intent authIntent = new Intent(NOTIFICATION_AUTO_FILL_INTENT);
|
||||
authIntent.putExtra(EXTRA_AUTH_INTENT_SENDER, intent);
|
||||
authIntent.putExtra(EXTRA_AUTH_FILL_IN_INTENT, fillInIntent);
|
||||
|
||||
final PendingIntent authPendingIntent = PendingIntent.getBroadcast(
|
||||
mContext, ++sResultCode, authIntent, PendingIntent.FLAG_ONE_SHOT);
|
||||
|
||||
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()))
|
||||
.setContentIntent(authPendingIntent);
|
||||
|
||||
ensureNotificationListener();
|
||||
|
||||
final long identity = Binder.clearCallingIdentity();
|
||||
try {
|
||||
NotificationManager.from(mContext).notify(0, notification.build());
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(identity);
|
||||
}
|
||||
}
|
||||
|
||||
@android.annotation.UiThread
|
||||
private void hideFillResponseAuthUiUiThread() {
|
||||
final long identity = Binder.clearCallingIdentity();
|
||||
try {
|
||||
NotificationManager.from(mContext).cancel(0);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(identity);
|
||||
}
|
||||
}
|
||||
|
||||
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. //
|
||||
/////////////////////////////////////////
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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 com.android.server.autofill;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
|
||||
/**
|
||||
* A view displaying the sign-in prompt for an auto-fill service.
|
||||
*/
|
||||
final class SignInPrompt extends Button {
|
||||
|
||||
SignInPrompt(Context context, CharSequence serviceName, View.OnClickListener listener) {
|
||||
super(context);
|
||||
// TODO(b/33197203): use strings.xml
|
||||
final String text = "Sign in to " + serviceName + " to autofill";
|
||||
|
||||
// TODO(b/33197203): polish UI / use better altenative than a button...
|
||||
|
||||
setText(text);
|
||||
setOnClickListener(listener);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user