From 8eab775d2cbc0d66f9b57451ec8a973f2464d014 Mon Sep 17 00:00:00 2001 From: Felipe Leme Date: Tue, 14 Feb 2017 11:27:23 -0800 Subject: [PATCH] R.I.P. temporary, zombie auto-fill notifications. They died already, but came back to life due to a last-minute API change. BUG: 33197203 Test: manual verification Test: CtsAutoFillServiceTestCases passes Change-Id: I4c248fd30ff087475116326d01e7c5a1c507cd74 --- .../autofill/AutoFillManagerServiceImpl.java | 65 +++++- .../android/server/autofill/AutoFillUI.java | 196 +++++------------- .../android/server/autofill/SignInPrompt.java | 38 ++++ 3 files changed, 144 insertions(+), 155 deletions(-) create mode 100644 services/autofill/java/com/android/server/autofill/SignInPrompt.java diff --git a/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java index 8d43dfbd180d2..85bf5c260b7b3 100644 --- a/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java @@ -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); diff --git a/services/autofill/java/com/android/server/autofill/AutoFillUI.java b/services/autofill/java/com/android/server/autofill/AutoFillUI.java index 97700405279e0..e83dc1e90137d 100644 --- a/services/autofill/java/com/android/server/autofill/AutoFillUI.java +++ b/services/autofill/java/com/android/server/autofill/AutoFillUI.java @@ -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. - * - *

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. // - ///////////////////////////////////////// } diff --git a/services/autofill/java/com/android/server/autofill/SignInPrompt.java b/services/autofill/java/com/android/server/autofill/SignInPrompt.java new file mode 100644 index 0000000000000..6d17acda4bdad --- /dev/null +++ b/services/autofill/java/com/android/server/autofill/SignInPrompt.java @@ -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); + } +}