Home | History | Annotate | Download | only in ui
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.server.autofill.ui;
     17 
     18 import static com.android.server.autofill.Helper.sDebug;
     19 import static com.android.server.autofill.Helper.sVerbose;
     20 
     21 import android.annotation.NonNull;
     22 import android.annotation.Nullable;
     23 import android.content.Context;
     24 import android.content.IntentSender;
     25 import android.graphics.drawable.Drawable;
     26 import android.metrics.LogMaker;
     27 import android.os.Bundle;
     28 import android.os.Handler;
     29 import android.os.IBinder;
     30 import android.os.RemoteException;
     31 import android.service.autofill.Dataset;
     32 import android.service.autofill.FillResponse;
     33 import android.service.autofill.SaveInfo;
     34 import android.service.autofill.ValueFinder;
     35 import android.text.TextUtils;
     36 import android.util.Slog;
     37 import android.view.autofill.AutofillId;
     38 import android.view.autofill.AutofillManager;
     39 import android.view.autofill.IAutofillWindowPresenter;
     40 import android.widget.Toast;
     41 
     42 import com.android.internal.logging.MetricsLogger;
     43 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
     44 import com.android.server.UiThread;
     45 import com.android.server.autofill.Helper;
     46 
     47 import java.io.PrintWriter;
     48 
     49 /**
     50  * Handles all autofill related UI tasks. The UI has two components:
     51  * fill UI that shows a popup style window anchored at the focused
     52  * input field for choosing a dataset to fill or trigger the response
     53  * authentication flow; save UI that shows a toast style window for
     54  * managing saving of user edits.
     55  */
     56 public final class AutoFillUI {
     57     private static final String TAG = "AutofillUI";
     58 
     59     private final Handler mHandler = UiThread.getHandler();
     60     private final @NonNull Context mContext;
     61 
     62     private @Nullable FillUi mFillUi;
     63     private @Nullable SaveUi mSaveUi;
     64 
     65     private @Nullable AutoFillUiCallback mCallback;
     66 
     67     private final MetricsLogger mMetricsLogger = new MetricsLogger();
     68 
     69     private final @NonNull OverlayControl mOverlayControl;
     70 
     71     public interface AutoFillUiCallback {
     72         void authenticate(int requestId, int datasetIndex, @NonNull IntentSender intent,
     73                 @Nullable Bundle extras);
     74         void fill(int requestId, int datasetIndex, @NonNull Dataset dataset);
     75         void save();
     76         void cancelSave();
     77         void requestShowFillUi(AutofillId id, int width, int height,
     78                 IAutofillWindowPresenter presenter);
     79         void requestHideFillUi(AutofillId id);
     80         void startIntentSender(IntentSender intentSender);
     81     }
     82 
     83     public AutoFillUI(@NonNull Context context) {
     84         mContext = context;
     85         mOverlayControl = new OverlayControl(context);
     86     }
     87 
     88     public void setCallback(@NonNull AutoFillUiCallback callback) {
     89         mHandler.post(() -> {
     90             if (mCallback != callback) {
     91                 if (mCallback != null) {
     92                     hideAllUiThread(mCallback);
     93                 }
     94 
     95                 mCallback = callback;
     96             }
     97         });
     98     }
     99 
    100     public void clearCallback(@NonNull AutoFillUiCallback callback) {
    101         mHandler.post(() -> {
    102             if (mCallback == callback) {
    103                 hideAllUiThread(callback);
    104                 mCallback = null;
    105             }
    106         });
    107     }
    108 
    109     /**
    110      * Displays an error message to the user.
    111      */
    112     public void showError(int resId, @NonNull AutoFillUiCallback callback) {
    113         showError(mContext.getString(resId), callback);
    114     }
    115 
    116     /**
    117      * Displays an error message to the user.
    118      */
    119     public void showError(@Nullable CharSequence message, @NonNull AutoFillUiCallback callback) {
    120         Slog.w(TAG, "showError(): " + message);
    121 
    122         mHandler.post(() -> {
    123             if (mCallback != callback) {
    124                 return;
    125             }
    126             hideAllUiThread(callback);
    127             if (!TextUtils.isEmpty(message)) {
    128                 Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
    129             }
    130         });
    131     }
    132 
    133     /**
    134      * Hides the fill UI.
    135      */
    136     public void hideFillUi(@NonNull AutoFillUiCallback callback) {
    137         mHandler.post(() -> hideFillUiUiThread(callback));
    138     }
    139 
    140     /**
    141      * Filters the options in the fill UI.
    142      *
    143      * @param filterText The filter prefix.
    144      */
    145     public void filterFillUi(@Nullable String filterText, @NonNull AutoFillUiCallback callback) {
    146         mHandler.post(() -> {
    147             if (callback != mCallback) {
    148                 return;
    149             }
    150             if (mFillUi != null) {
    151                 mFillUi.setFilterText(filterText);
    152             }
    153         });
    154     }
    155 
    156     /**
    157      * Shows the fill UI, removing the previous fill UI if the has changed.
    158      *
    159      * @param focusedId the currently focused field
    160      * @param response the current fill response
    161      * @param filterText text of the view to be filled
    162      * @param servicePackageName package name of the autofill service filling the activity
    163      * @param packageName package name of the activity that is filled
    164      * @param callback Identifier for the caller
    165      */
    166     public void showFillUi(@NonNull AutofillId focusedId, @NonNull FillResponse response,
    167             @Nullable String filterText, @Nullable String servicePackageName,
    168             @NonNull String packageName, @NonNull AutoFillUiCallback callback) {
    169         if (sDebug) {
    170             final int size = filterText == null ? 0 : filterText.length();
    171             Slog.d(TAG, "showFillUi(): id=" + focusedId + ", filter=" + size + " chars");
    172         }
    173         final LogMaker log =
    174                 Helper.newLogMaker(MetricsEvent.AUTOFILL_FILL_UI, packageName, servicePackageName)
    175                 .addTaggedData(MetricsEvent.FIELD_AUTOFILL_FILTERTEXT_LEN,
    176                         filterText == null ? 0 : filterText.length())
    177                 .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_DATASETS,
    178                         response.getDatasets() == null ? 0 : response.getDatasets().size());
    179 
    180         mHandler.post(() -> {
    181             if (callback != mCallback) {
    182                 return;
    183             }
    184             hideAllUiThread(callback);
    185             mFillUi = new FillUi(mContext, response, focusedId,
    186                     filterText, mOverlayControl, new FillUi.Callback() {
    187                 @Override
    188                 public void onResponsePicked(FillResponse response) {
    189                     log.setType(MetricsEvent.TYPE_DETAIL);
    190                     hideFillUiUiThread(callback);
    191                     if (mCallback != null) {
    192                         mCallback.authenticate(response.getRequestId(),
    193                                 AutofillManager.AUTHENTICATION_ID_DATASET_ID_UNDEFINED,
    194                                 response.getAuthentication(), response.getClientState());
    195                     }
    196                 }
    197 
    198                 @Override
    199                 public void onDatasetPicked(Dataset dataset) {
    200                     log.setType(MetricsEvent.TYPE_ACTION);
    201                     hideFillUiUiThread(callback);
    202                     if (mCallback != null) {
    203                         final int datasetIndex = response.getDatasets().indexOf(dataset);
    204                         mCallback.fill(response.getRequestId(), datasetIndex, dataset);
    205                     }
    206                 }
    207 
    208                 @Override
    209                 public void onCanceled() {
    210                     log.setType(MetricsEvent.TYPE_DISMISS);
    211                     hideFillUiUiThread(callback);
    212                 }
    213 
    214                 @Override
    215                 public void onDestroy() {
    216                     if (log.getType() == MetricsEvent.TYPE_UNKNOWN) {
    217                         log.setType(MetricsEvent.TYPE_CLOSE);
    218                     }
    219                     mMetricsLogger.write(log);
    220                 }
    221 
    222                 @Override
    223                 public void requestShowFillUi(int width, int height,
    224                         IAutofillWindowPresenter windowPresenter) {
    225                     if (mCallback != null) {
    226                         mCallback.requestShowFillUi(focusedId, width, height, windowPresenter);
    227                     }
    228                 }
    229 
    230                 @Override
    231                 public void requestHideFillUi() {
    232                     if (mCallback != null) {
    233                         mCallback.requestHideFillUi(focusedId);
    234                     }
    235                 }
    236 
    237                 @Override
    238                 public void startIntentSender(IntentSender intentSender) {
    239                     if (mCallback != null) {
    240                         mCallback.startIntentSender(intentSender);
    241                     }
    242                 }
    243             });
    244         });
    245     }
    246 
    247     /**
    248      * Shows the UI asking the user to save for autofill.
    249      */
    250     public void showSaveUi(@NonNull CharSequence serviceLabel, @NonNull Drawable serviceIcon,
    251             @Nullable String servicePackageName, @NonNull SaveInfo info,
    252             @NonNull ValueFinder valueFinder, @NonNull String packageName,
    253             @NonNull AutoFillUiCallback callback, @NonNull PendingUi pendingSaveUi) {
    254         if (sVerbose) Slog.v(TAG, "showSaveUi() for " + packageName + ": " + info);
    255         int numIds = 0;
    256         numIds += info.getRequiredIds() == null ? 0 : info.getRequiredIds().length;
    257         numIds += info.getOptionalIds() == null ? 0 : info.getOptionalIds().length;
    258 
    259         final LogMaker log =
    260                 Helper.newLogMaker(MetricsEvent.AUTOFILL_SAVE_UI, packageName, servicePackageName)
    261                 .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_IDS, numIds);
    262 
    263         mHandler.post(() -> {
    264             if (callback != mCallback) {
    265                 return;
    266             }
    267             hideAllUiThread(callback);
    268             mSaveUi = new SaveUi(mContext, pendingSaveUi, serviceLabel, serviceIcon,
    269                     servicePackageName, packageName, info, valueFinder, mOverlayControl,
    270                     new SaveUi.OnSaveListener() {
    271                 @Override
    272                 public void onSave() {
    273                     log.setType(MetricsEvent.TYPE_ACTION);
    274                     hideSaveUiUiThread(mCallback);
    275                     if (mCallback != null) {
    276                         mCallback.save();
    277                     }
    278                     destroySaveUiUiThread(pendingSaveUi, true);
    279                 }
    280 
    281                 @Override
    282                 public void onCancel(IntentSender listener) {
    283                     log.setType(MetricsEvent.TYPE_DISMISS);
    284                     hideSaveUiUiThread(mCallback);
    285                     if (listener != null) {
    286                         try {
    287                             listener.sendIntent(mContext, 0, null, null, null);
    288                         } catch (IntentSender.SendIntentException e) {
    289                             Slog.e(TAG, "Error starting negative action listener: "
    290                                     + listener, e);
    291                         }
    292                     }
    293                     if (mCallback != null) {
    294                         mCallback.cancelSave();
    295                     }
    296                     destroySaveUiUiThread(pendingSaveUi, true);
    297                 }
    298 
    299                 @Override
    300                 public void onDestroy() {
    301                     if (log.getType() == MetricsEvent.TYPE_UNKNOWN) {
    302                         log.setType(MetricsEvent.TYPE_CLOSE);
    303 
    304                         if (mCallback != null) {
    305                             mCallback.cancelSave();
    306                         }
    307                     }
    308                     mMetricsLogger.write(log);
    309                 }
    310             });
    311         });
    312     }
    313 
    314     /**
    315      * Executes an operation in the pending save UI, if any.
    316      */
    317     public void onPendingSaveUi(int operation, @NonNull IBinder token) {
    318         mHandler.post(() -> {
    319             if (mSaveUi != null) {
    320                 mSaveUi.onPendingUi(operation, token);
    321             } else {
    322                 Slog.w(TAG, "onPendingSaveUi(" + operation + "): no save ui");
    323             }
    324         });
    325     }
    326 
    327     /**
    328      * Hides all UI affordances.
    329      */
    330     public void hideAll(@Nullable AutoFillUiCallback callback) {
    331         mHandler.post(() -> hideAllUiThread(callback));
    332     }
    333 
    334     /**
    335      * Destroy all UI affordances.
    336      */
    337     public void destroyAll(@Nullable PendingUi pendingSaveUi,
    338             @Nullable AutoFillUiCallback callback, boolean notifyClient) {
    339         mHandler.post(() -> destroyAllUiThread(pendingSaveUi, callback, notifyClient));
    340     }
    341 
    342     public void dump(PrintWriter pw) {
    343         pw.println("Autofill UI");
    344         final String prefix = "  ";
    345         final String prefix2 = "    ";
    346         if (mFillUi != null) {
    347             pw.print(prefix); pw.println("showsFillUi: true");
    348             mFillUi.dump(pw, prefix2);
    349         } else {
    350             pw.print(prefix); pw.println("showsFillUi: false");
    351         }
    352         if (mSaveUi != null) {
    353             pw.print(prefix); pw.println("showsSaveUi: true");
    354             mSaveUi.dump(pw, prefix2);
    355         } else {
    356             pw.print(prefix); pw.println("showsSaveUi: false");
    357         }
    358     }
    359 
    360     @android.annotation.UiThread
    361     private void hideFillUiUiThread(@Nullable AutoFillUiCallback callback) {
    362         if (mFillUi != null && (callback == null || callback == mCallback)) {
    363             mFillUi.destroy();
    364             mFillUi = null;
    365         }
    366     }
    367 
    368     @android.annotation.UiThread
    369     @Nullable
    370     private PendingUi hideSaveUiUiThread(@Nullable AutoFillUiCallback callback) {
    371         if (sVerbose) {
    372             Slog.v(TAG, "hideSaveUiUiThread(): mSaveUi=" + mSaveUi + ", callback=" + callback
    373                     + ", mCallback=" + mCallback);
    374         }
    375         if (mSaveUi != null && (callback == null || callback == mCallback)) {
    376             return mSaveUi.hide();
    377         }
    378         return null;
    379     }
    380 
    381     @android.annotation.UiThread
    382     private void destroySaveUiUiThread(@Nullable PendingUi pendingSaveUi, boolean notifyClient) {
    383         if (mSaveUi == null) {
    384             // Calling destroySaveUiUiThread() twice is normal - it usually happens when the
    385             // first call is made after the SaveUI is hidden and the second when the session is
    386             // finished.
    387             if (sDebug) Slog.d(TAG, "destroySaveUiUiThread(): already destroyed");
    388             return;
    389         }
    390 
    391         if (sDebug) Slog.d(TAG, "destroySaveUiUiThread(): " + pendingSaveUi);
    392         mSaveUi.destroy();
    393         mSaveUi = null;
    394         if (pendingSaveUi != null && notifyClient) {
    395             try {
    396                 if (sDebug) Slog.d(TAG, "destroySaveUiUiThread(): notifying client");
    397                 pendingSaveUi.client.setSaveUiState(pendingSaveUi.id, false);
    398             } catch (RemoteException e) {
    399                 Slog.e(TAG, "Error notifying client to set save UI state to hidden: " + e);
    400             }
    401         }
    402     }
    403 
    404     @android.annotation.UiThread
    405     private void destroyAllUiThread(@Nullable PendingUi pendingSaveUi,
    406             @Nullable AutoFillUiCallback callback, boolean notifyClient) {
    407         hideFillUiUiThread(callback);
    408         destroySaveUiUiThread(pendingSaveUi, notifyClient);
    409     }
    410 
    411     @android.annotation.UiThread
    412     private void hideAllUiThread(@Nullable AutoFillUiCallback callback) {
    413         hideFillUiUiThread(callback);
    414         final PendingUi pendingSaveUi = hideSaveUiUiThread(callback);
    415         if (pendingSaveUi != null && pendingSaveUi.getState() == PendingUi.STATE_FINISHED) {
    416             if (sDebug) {
    417                 Slog.d(TAG, "hideAllUiThread(): "
    418                         + "destroying Save UI because pending restoration is finished");
    419             }
    420             destroySaveUiUiThread(pendingSaveUi, true);
    421         }
    422     }
    423 }
    424