/* * 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 foo.bar.fill; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.PendingIntent; import android.app.assist.AssistStructure; import android.app.assist.AssistStructure.WindowNode; import android.app.assist.AssistStructure.ViewNode; import android.content.Intent; import android.content.IntentSender; import android.content.pm.PackageManager; import android.os.CancellationSignal; import android.service.autofill.AutofillService; import android.service.autofill.Dataset; import android.service.autofill.FillCallback; import android.service.autofill.FillRequest; import android.service.autofill.FillResponse; import android.service.autofill.SaveCallback; import android.service.autofill.SaveInfo; import android.service.autofill.SaveRequest; import android.util.Log; import android.view.View; import android.view.autofill.AutofillId; import android.view.autofill.AutofillManager; import android.view.autofill.AutofillValue; import android.widget.EditText; import android.widget.RemoteViews; import java.util.ArrayList; import java.util.List; import java.util.function.Predicate; import android.widget.TextView; import foo.bar.fill.R; public class FillService extends AutofillService { private static final String LOG_TAG = "FillService"; static final boolean TEST_RESPONSE_AUTH = false; public static final String RESPONSE_ID = "RESPONSE_ID"; static final String DATASET1_NAME = "Foo"; static final String DATASET1_USERNAME = "Foo"; static final String DATASET1_PASSWORD = "1"; static final String DATASET2_NAME = "Bar"; static final String DATASET2_USERNAME = "Bar"; static final String DATASET2_PASSWORD = "12"; static final String DATASET3_NAME = "Baz"; static final String DATASET3_USERNAME = "Baz"; static final String DATASET3_PASSWORD = "123"; static final String DATASET4_NAME = "Bam"; static final String DATASET4_USERNAME = "Bam"; static final String DATASET4_PASSWORD = "1234"; static final String DATASET5_NAME = "Bak"; static final String DATASET5_USERNAME = "Bak"; static final String DATASET5_PASSWORD = "12345"; static final String EXTRA_RESPONSE_ID = "foo.bar.fill.extra.RESPONSE_ID"; static final String EXTRA_DATASET_ID = "foo.bar.fill.extra.DATASET_ID"; @Override public void onFillRequest(@NonNull FillRequest request, @NonNull CancellationSignal cancellationSignal, @NonNull FillCallback callback) { AssistStructure structure = request.getFillContexts().get(0).getStructure(); dumpNodeTree(structure); // ViewNode username = findUsername(structure); // ViewNode password = findPassword(structure); ViewNode username = null; ViewNode password = null; final List inputs = findTextInputs(structure); if (inputs.size() > 1) { username = inputs.get(0); password = inputs.get(1); } Log.i(LOG_TAG, "found username+username:" + (username != null && password != null)); if (username != null && password != null) { final FillResponse response; if (TEST_RESPONSE_AUTH) { Intent intent = new Intent(this, AuthActivity.class); intent.putExtra(EXTRA_RESPONSE_ID, RESPONSE_ID); IntentSender sender = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT) .getIntentSender(); RemoteViews presentation = new RemoteViews(getPackageName(), R.layout.pathology); // presentation.setTextViewText(R.id.text1, "First"); // Intent firstIntent = new Intent(this, FirstActivity.class); // presentation.setOnClickPendingIntent(R.id.text1, PendingIntent.getActivity( // this, 0, firstIntent, PendingIntent.FLAG_CANCEL_CURRENT)); // presentation.setTextViewText(R.id.text2, "Second"); // Intent secondIntent = new Intent(this, SecondActivity.class); // presentation.setOnClickPendingIntent(R.id.text2, PendingIntent.getActivity( // this, 0, secondIntent, PendingIntent.FLAG_CANCEL_CURRENT)); response = new FillResponse.Builder() .setAuthentication(new AutofillId[]{username.getAutofillId(), password.getAutofillId()}, sender, presentation) .build(); } else { Intent intent = new Intent(this, AuthActivity.class); intent.putExtra(EXTRA_DATASET_ID, DATASET1_NAME); IntentSender sender = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT) .getIntentSender(); RemoteViews presentation1 = new RemoteViews(getPackageName(), R.layout.list_item); presentation1.setTextViewText(R.id.text1, DATASET1_NAME); RemoteViews presentation2 = new RemoteViews(getPackageName(), R.layout.list_item); presentation2.setTextViewText(R.id.text1, DATASET2_NAME); RemoteViews presentation3 = new RemoteViews(getPackageName(), R.layout.list_item); presentation3.setTextViewText(R.id.text1, DATASET3_NAME); RemoteViews presentation4 = new RemoteViews(getPackageName(), R.layout.list_item); presentation4.setTextViewText(R.id.text1, DATASET4_NAME); RemoteViews presentation5 = new RemoteViews(getPackageName(), R.layout.list_item); presentation5.setTextViewText(R.id.text1, /*DATASET5_NAME*/ "Auth needed"); response = new FillResponse.Builder() .addDataset(new Dataset.Builder(presentation1) .setValue(username.getAutofillId(), AutofillValue.forText(DATASET1_USERNAME)) .setValue(password.getAutofillId(), AutofillValue.forText(DATASET1_PASSWORD)) .build()) // .addDataset(new Dataset.Builder(presentation2) // .setValue(username.getAutofillId(), // AutofillValue.forText(DATASET2_USERNAME)) // .setValue(password.getAutofillId(), // AutofillValue.forText(DATASET2_PASSWORD)) //// .setAuthentication(sender) // .build()) // .addDataset(new Dataset.Builder(presentation3) // .setValue(username.getAutofillId(), // AutofillValue.forText(DATASET3_USERNAME)) // .setValue(password.getAutofillId(), // AutofillValue.forText(DATASET3_PASSWORD)) //// .setAuthentication(sender) // .build()) // .addDataset(new Dataset.Builder(presentation4) // .setValue(username.getAutofillId(), // AutofillValue.forText(DATASET4_USERNAME)) // .setValue(password.getAutofillId(), // AutofillValue.forText(DATASET4_PASSWORD)) //// .setAuthentication(sender) // .build()) // .addDataset(new Dataset.Builder(presentation5) // .setValue(username.getAutofillId(), // AutofillValue.forText(DATASET5_USERNAME)) // .setValue(password.getAutofillId(), // AutofillValue.forText(DATASET5_PASSWORD)) // .setAuthentication(sender) // .build()) .setSaveInfo(new SaveInfo.Builder( SaveInfo.SAVE_DATA_TYPE_PASSWORD | SaveInfo.SAVE_DATA_TYPE_USERNAME, new AutofillId[] {username.getAutofillId(), password.getAutofillId()}) .setFlags(SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE) .build()) .build(); } callback.onSuccess(response); } else { //callback.onFailure("Whoops"); callback.onSuccess(null); } } @Override public void onSaveRequest(@NonNull SaveRequest request, @NonNull SaveCallback callback) { AssistStructure structure = request.getFillContexts().get(0).getStructure(); ViewNode username = findUsername(structure); ViewNode password = findPassword(structure); } static void dumpNodeTree(AssistStructure structure) { findByPredicate(structure, (node) -> { if (node.getAutofillValue() != null) { Log.e("class:" + LOG_TAG, node.getClassName() + " value:" + node.getAutofillValue()); } // Log.e(LOG_TAG, (node.getAutofillValue() != null && node.getAutofillValue().isText()) // ? node.getAutofillValue().getTextValue().toString() + "-" +node.getAutofillId() : "NOPE"); return false; }); } List findTextInputs(AssistStructure structure) { final List inputs = new ArrayList<>(); findByPredicate(structure, (node) -> { if (node.getClassName().equals(EditText.class.getName())) { inputs.add(node); } return false; }); return inputs; } static ViewNode findUsername(AssistStructure structure) { return findByPredicate(structure, (node) -> node.getAutofillType() == View.AUTOFILL_TYPE_TEXT && (autofillTextValueContains(node, "username") || "username".equals(node.getIdEntry())) ); } static ViewNode findPassword(AssistStructure structure) { return findByPredicate(structure, (node) -> node.getAutofillType() == View.AUTOFILL_TYPE_TEXT && (autofillTextValueContains(node, "password") || "password".equals(node.getIdEntry())) ); } private static boolean autofillTextValueContains(ViewNode node, String text) { return node.getAutofillValue() != null && node.getAutofillValue().getTextValue() != null && node.getAutofillValue().getTextValue().toString().toLowerCase() .contains(text.toLowerCase()); } private static ViewNode findByPredicate(AssistStructure structure, Predicate predicate) { final int windowCount = structure.getWindowNodeCount(); for (int i = 0; i < windowCount; i++) { WindowNode window = structure.getWindowNodeAt(i); ViewNode root = window.getRootViewNode(); if (root == null) { return null; } ViewNode node = findByPredicate(root, predicate); if (node != null) { return node; } } return null; } private static ViewNode findByPredicate(ViewNode root, Predicate predicate) { if (root == null) { return null; } if (predicate.test(root)) { return root; } final int childCount = root.getChildCount(); for (int i = 0; i < childCount; i++) { ViewNode child = root.getChildAt(i); ViewNode node = findByPredicate(child, predicate); if (node != null) { return node; } } return null; } }