Merge "R.I.P. temporary, zombie auto-fill notifications."
This commit is contained in:
committed by
Android (Google) Code Review
commit
b454cb8842
@@ -39,6 +39,7 @@ import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentSender;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.graphics.Rect;
|
||||
@@ -181,6 +182,23 @@ final class AutoFillManagerServiceImpl {
|
||||
updateLocked();
|
||||
}
|
||||
|
||||
CharSequence getServiceName() {
|
||||
if (mInfo == null) {
|
||||
return null;
|
||||
}
|
||||
final ComponentName serviceComponent = mInfo.getServiceInfo().getComponentName();
|
||||
final String packageName = serviceComponent.getPackageName();
|
||||
|
||||
try {
|
||||
final PackageManager pm = mContext.getPackageManager();
|
||||
final ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
|
||||
return pm.getApplicationLabel(info);
|
||||
} catch (Exception e) {
|
||||
Slog.w(TAG, "Could not get label for " + packageName + ": " + e);
|
||||
return packageName;
|
||||
}
|
||||
}
|
||||
|
||||
void updateLocked() {
|
||||
ComponentName serviceComponent = null;
|
||||
ServiceInfo serviceInfo = null;
|
||||
@@ -438,14 +456,20 @@ 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 and session if it was an inner
|
||||
// class of Session...
|
||||
private final Session mSession;
|
||||
// TODO(b/33197203): encapsulate access so it's not called by UI
|
||||
FillResponse mResponse;
|
||||
Intent mAuthIntent;
|
||||
|
||||
private AutoFillValue mAutoFillValue;
|
||||
private Rect mBounds;
|
||||
|
||||
private boolean mValueUpdated;
|
||||
|
||||
ViewState(AutoFillId id, Listener listener) {
|
||||
ViewState(Session session, AutoFillId id, Listener listener) {
|
||||
mSession = session;
|
||||
mId = id;
|
||||
mListener = listener;
|
||||
}
|
||||
@@ -458,6 +482,18 @@ final class AutoFillManagerServiceImpl {
|
||||
maybeCallOnFillReady();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used when a {@link FillResponse} requires authentication to be unlocked.
|
||||
*/
|
||||
void setResponse(FillResponse response, Intent authIntent) {
|
||||
mAuthIntent = authIntent;
|
||||
setResponse(response);
|
||||
}
|
||||
|
||||
CharSequence getServiceName() {
|
||||
return mSession.getServiceName();
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -495,6 +531,7 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,7 +602,6 @@ final class AutoFillManagerServiceImpl {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// FillServiceCallbacks
|
||||
@Override
|
||||
public void onFillRequestSuccess(FillResponse response) {
|
||||
@@ -763,7 +799,7 @@ final class AutoFillManagerServiceImpl {
|
||||
|
||||
ViewState viewState = mViewStates.get(id);
|
||||
if (viewState == null) {
|
||||
viewState = new ViewState(id, this);
|
||||
viewState = new ViewState(this, id, this);
|
||||
mViewStates.put(id, viewState);
|
||||
}
|
||||
|
||||
@@ -844,13 +880,19 @@ 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) {
|
||||
// Handle authentication.
|
||||
final Intent fillInIntent = createAuthFillInIntent(mStructure);
|
||||
getUiForShowing().showFillResponseAuthRequest(
|
||||
mCurrentResponse.getAuthentication(), fillInIntent);
|
||||
|
||||
mCurrentViewState.setResponse(mCurrentResponse, fillInIntent);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -864,10 +906,7 @@ final class AutoFillManagerServiceImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO(b/33197203): Consider using mCurrentResponse, depends on partitioning design
|
||||
if (mCurrentViewState != null) {
|
||||
mCurrentViewState.setResponse(mCurrentResponse);
|
||||
}
|
||||
mCurrentViewState.setResponse(mCurrentResponse);
|
||||
}
|
||||
|
||||
void autoFill(Dataset dataset) {
|
||||
@@ -884,6 +923,10 @@ final class AutoFillManagerServiceImpl {
|
||||
}
|
||||
}
|
||||
|
||||
CharSequence getServiceName() {
|
||||
return AutoFillManagerServiceImpl.this.getServiceName();
|
||||
}
|
||||
|
||||
private Intent createAuthFillInIntent(AssistStructure structure) {
|
||||
Intent fillInIntent = new Intent();
|
||||
fillInIntent.putExtra(AutoFillManager.EXTRA_ASSIST_STRUCTURE, structure);
|
||||
|
||||
@@ -56,19 +56,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;
|
||||
@@ -156,63 +152,76 @@ 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;
|
||||
if (viewState.mAuthIntent != null) {
|
||||
final CharSequence serviceName = viewState.getServiceName();
|
||||
|
||||
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();
|
||||
});
|
||||
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 (datasetPicker != null) {
|
||||
datasetPicker.update(filterText);
|
||||
}
|
||||
|
||||
if (DEBUG) Slog.d(TAG, "showFillUi(): bounds=" + bounds + ", filterText=" + filterText);
|
||||
mFillView.update(filterText);
|
||||
mFillWindow.show(bounds);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows an UI affordance indicating that user action is required before a {@link
|
||||
* android.service.autofill.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);
|
||||
}
|
||||
|
||||
@@ -250,14 +259,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,103 +317,4 @@ final class AutoFillUI {
|
||||
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,38 @@
|
||||
/*
|
||||
* 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 = serviceName != null
|
||||
? "Sign in to " + serviceName + " to autofill"
|
||||
: "Sign in to autofill";
|
||||
|
||||
// TODO(b/33197203): polish UI / use better altenative than a button...
|
||||
setText(text);
|
||||
setOnClickListener(listener);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user