Merge "R.I.P. temporary, zombie auto-fill notifications."

This commit is contained in:
TreeHugger Robot
2017-02-14 22:10:38 +00:00
committed by Android (Google) Code Review
3 changed files with 144 additions and 155 deletions

View File

@@ -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);

View File

@@ -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. //
/////////////////////////////////////////
}

View File

@@ -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);
}
}