|  | /* | 
|  | * Copyright (C) 2019 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.example.android.inlinefillservice; | 
|  |  | 
|  | import android.content.Context; | 
|  | import android.content.IntentSender; | 
|  | import android.os.CancellationSignal; | 
|  | import android.service.autofill.AutofillService; | 
|  | import android.service.autofill.FillCallback; | 
|  | import android.service.autofill.FillRequest; | 
|  | import android.service.autofill.FillResponse; | 
|  | import android.service.autofill.InlinePresentation; | 
|  | import android.service.autofill.Presentations; | 
|  | import android.service.autofill.SaveCallback; | 
|  | import android.service.autofill.SaveInfo; | 
|  | import android.service.autofill.SaveRequest; | 
|  | import android.service.autofill.SavedDatasetsInfo; | 
|  | import android.service.autofill.SavedDatasetsInfoCallback; | 
|  | import android.util.ArrayMap; | 
|  | import android.util.Log; | 
|  | import android.view.autofill.AutofillId; | 
|  | import android.view.inputmethod.InlineSuggestionsRequest; | 
|  | import android.widget.RemoteViews; | 
|  |  | 
|  | import androidx.annotation.NonNull; | 
|  |  | 
|  | import java.util.Collection; | 
|  | import java.util.Collections; | 
|  | import java.util.Optional; | 
|  |  | 
|  | /** | 
|  | * A basic {@link AutofillService} implementation that only shows dynamic-generated datasets | 
|  | * and supports inline suggestions. | 
|  | */ | 
|  | public class InlineFillService extends AutofillService { | 
|  |  | 
|  | static final String TAG = "InlineFillService"; | 
|  |  | 
|  | /** | 
|  | * Number of datasets sent on each request - we're simple, that value is hardcoded in our DNA! | 
|  | */ | 
|  | static final int NUMBER_DATASETS = 6; | 
|  |  | 
|  | private final boolean mAuthenticateResponses = false; | 
|  | private final boolean mAuthenticateDatasets = false; | 
|  |  | 
|  | @Override | 
|  | public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal, | 
|  | FillCallback callback) { | 
|  | Log.d(TAG, "onFillRequest()"); | 
|  |  | 
|  | final Context context = getApplicationContext(); | 
|  |  | 
|  | // Find autofillable fields | 
|  | ArrayMap<String, AutofillId> fields = Helper.getAutofillableFields(request); | 
|  | Log.d(TAG, "autofillable fields:" + fields); | 
|  | if (fields.isEmpty()) { | 
|  | Helper.showMessage(context, | 
|  | "InlineFillService could not figure out how to autofill this screen"); | 
|  | callback.onSuccess(null); | 
|  | return; | 
|  | } | 
|  | final Optional<InlineSuggestionsRequest> inlineRequest = | 
|  | InlineRequestHelper.getInlineSuggestionsRequest(request); | 
|  | final int maxSuggestionsCount = InlineRequestHelper.getMaxSuggestionCount(inlineRequest, | 
|  | NUMBER_DATASETS); | 
|  |  | 
|  | // Create the base response | 
|  | final FillResponse response; | 
|  | if (mAuthenticateResponses) { | 
|  | int size = fields.size(); | 
|  | String[] hints = new String[size]; | 
|  | AutofillId[] ids = new AutofillId[size]; | 
|  | for (int i = 0; i < size; i++) { | 
|  | hints[i] = fields.keyAt(i); | 
|  | ids[i] = fields.valueAt(i); | 
|  | } | 
|  | IntentSender authentication = AuthActivity.newIntentSenderForResponse(this, hints, | 
|  | ids, mAuthenticateDatasets, inlineRequest.orElse(null)); | 
|  | RemoteViews presentation = ResponseHelper.newDatasetPresentation(getPackageName(), | 
|  | "Tap to auth response"); | 
|  |  | 
|  | InlinePresentation inlinePresentation = | 
|  | InlineRequestHelper.maybeCreateInlineAuthenticationResponse(context, | 
|  | inlineRequest); | 
|  | final Presentations.Builder fieldPresentationsBuilder = | 
|  | new Presentations.Builder(); | 
|  | fieldPresentationsBuilder.setMenuPresentation(presentation); | 
|  | fieldPresentationsBuilder.setInlinePresentation(inlinePresentation); | 
|  | response = new FillResponse.Builder() | 
|  | .setAuthentication(ids, authentication, fieldPresentationsBuilder.build()) | 
|  | .build(); | 
|  | } else { | 
|  | response = createResponse(this, fields, maxSuggestionsCount, mAuthenticateDatasets, | 
|  | inlineRequest); | 
|  | } | 
|  | callback.onSuccess(response); | 
|  | } | 
|  |  | 
|  | static FillResponse createResponse(@NonNull Context context, | 
|  | @NonNull ArrayMap<String, AutofillId> fields, int numDatasets, | 
|  | boolean authenticateDatasets, | 
|  | @NonNull Optional<InlineSuggestionsRequest> inlineRequest) { | 
|  | String packageName = context.getPackageName(); | 
|  | FillResponse.Builder response = new FillResponse.Builder(); | 
|  | // 1.Add the dynamic datasets | 
|  | for (int i = 0; i < numDatasets; i++) { | 
|  | if (authenticateDatasets) { | 
|  | response.addDataset(ResponseHelper.newLockedDataset(context, fields, packageName, i, | 
|  | inlineRequest)); | 
|  | } else { | 
|  | response.addDataset(ResponseHelper.newUnlockedDataset(context, fields, | 
|  | packageName, i, inlineRequest)); | 
|  | } | 
|  | } | 
|  |  | 
|  | // 2. Add some inline actions | 
|  | if (inlineRequest.isPresent()) { | 
|  | response.addDataset(InlineRequestHelper.createInlineActionDataset(context, fields, | 
|  | inlineRequest.get(), R.drawable.ic_settings)); | 
|  | response.addDataset(InlineRequestHelper.createInlineActionDataset(context, fields, | 
|  | inlineRequest.get(), R.drawable.ic_settings)); | 
|  | } | 
|  | // 3. Add fill dialog | 
|  | RemoteViews dialogPresentation = | 
|  | ResponseHelper.newDatasetPresentation(packageName, "Dialog Header"); | 
|  | response.setDialogHeader(dialogPresentation); | 
|  | response.setFillDialogTriggerIds(fields.valueAt(0), fields.valueAt(1)); | 
|  |  | 
|  | // 4.Add save info | 
|  | Collection<AutofillId> ids = fields.values(); | 
|  | AutofillId[] requiredIds = new AutofillId[ids.size()]; | 
|  | ids.toArray(requiredIds); | 
|  | response.setSaveInfo( | 
|  | // We're simple, so we're generic | 
|  | new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_GENERIC, requiredIds).build()); | 
|  |  | 
|  | // 5.Profit! | 
|  | return response.build(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onSaveRequest(SaveRequest request, SaveCallback callback) { | 
|  | Log.d(TAG, "onSaveRequest()"); | 
|  | Helper.showMessage(getApplicationContext(), "InlineFillService doesn't support Save"); | 
|  | callback.onSuccess(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onSavedDatasetsInfoRequest(@NonNull SavedDatasetsInfoCallback callback) { | 
|  | callback.onSuccess( | 
|  | Collections.singleton( | 
|  | new SavedDatasetsInfo( | 
|  | SavedDatasetsInfo.TYPE_PASSWORDS, sNumSavedDatasets))); | 
|  | sNumSavedDatasets++; | 
|  | sNumSavedDatasets %= 3; | 
|  | } | 
|  |  | 
|  | private static int sNumSavedDatasets = 0; | 
|  | } |