Update the sample keyboard to showcase scroll, clip, toggle am: f34c6275f1

Change-Id: I5a636d05e66959a4d627dfb41ebd7a85e913d939
This commit is contained in:
Svet Ganov
2020-04-08 03:23:47 +00:00
committed by Automerger Merge Worker
7 changed files with 490 additions and 128 deletions

View File

@@ -22,14 +22,43 @@
xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/KeyboardArea">
<HorizontalScrollView
android:layout_width="match_parent"
<LinearLayout
android:id="@+id/suggestion_strip"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/suggestion_view"
style="@style/KeyboardRow.Header">
android:id="@+id/pinned_suggestions_start"
style="@style/PinnedSuggestionArea">
</LinearLayout>
</HorizontalScrollView>
<com.example.android.autofillkeyboard.InlineContentClipView
android:id="@+id/scrollable_suggestions_clip"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_weight="1">
<HorizontalScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/scrollable_suggestions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/KeyboardRow.Header">
</LinearLayout>
</HorizontalScrollView>
</com.example.android.autofillkeyboard.InlineContentClipView>
<LinearLayout
android:id="@+id/pinned_suggestions_end"
style="@style/PinnedSuggestionArea">
</LinearLayout>
</LinearLayout>
<LinearLayout style="@style/KeyboardRow">
<TextView
@@ -141,4 +170,5 @@
android:id="@+id/key_pos_enter"
style="@style/SoftKey.Function"/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2020 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.
-->
<resources>
<color name="suggestion_strip_background">#FFEEEEEE</color>
</resources>

View File

@@ -22,6 +22,6 @@
<dimen name="text_size_normal">24dp</dimen>
<dimen name="text_size_symbol">14dp</dimen>
<dimen name="keyboard_header_height">41dp</dimen>
<dimen name="keyboard_header_height">48dp</dimen>
<dimen name="keyboard_row_height">48dp</dimen>
</resources>

View File

@@ -25,18 +25,26 @@
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_gravity">bottom</item>
<item name="android:orientation">vertical</item>
<item name="android:background">#FFFFFFFF</item>
<item name="android:gravity">bottom</item>
</style>
<style name="KeyboardRow">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">@dimen/keyboard_row_height</item>
<item name="android:orientation">horizontal</item>
<item name="android:background">#FFFFFFFF</item>
</style>
<style name="PinnedSuggestionArea">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">@dimen/keyboard_header_height</item>
<item name="android:layout_weight">0</item>
<item name="android:background">@color/suggestion_strip_background</item>
</style>
<style name="KeyboardRow.Header">
<item name="android:layout_height">@dimen/keyboard_header_height</item>
<item name="android:background">#FFEEEEEE</item>
<item name="android:background">@android:color/transparent</item>
</style>
<style name="SoftKey">

View File

@@ -17,34 +17,73 @@
package com.example.android.autofillkeyboard;
import android.inputmethodservice.InputMethodService;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.GuardedBy;
import android.util.Log;
import android.util.Size;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.inline.InlinePresentationSpec;
import android.view.inputmethod.EditorInfo;
import android.widget.inline.InlineContentView;
import android.widget.inline.InlinePresentationSpec;
import android.view.inputmethod.InlineSuggestion;
import android.view.inputmethod.InlineSuggestionsRequest;
import android.view.inputmethod.InlineSuggestionsResponse;
import android.widget.LinearLayout;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/** The {@link InputMethodService} implementation for Autofill keyboard. */
public class AutofillImeService extends InputMethodService {
private static final boolean SHOWCASE_BG_FG_TRANSITION = false;
// To test this you need to change KeyboardArea style layout_height to 400dp
private static final boolean SHOWCASE_UP_DOWN_TRANSITION = false;
private static final long MOVE_SUGGESTIONS_TO_BG_TIMEOUT = 5000;
private static final long MOVE_SUGGESTIONS_TO_FG_TIMEOUT = 15000;
private static final long MOVE_SUGGESTIONS_UP_TIMEOUT = 5000;
private static final long MOVE_SUGGESTIONS_DOWN_TIMEOUT = 10000;
private InputView mInputView;
private Decoder mDecoder;
private LinearLayout mSuggestionStrip;
private ViewGroup mSuggestionStrip;
private ViewGroup mPinnedSuggestionsStart;
private ViewGroup mPinnedSuggestionsEnd;
private InlineContentClipView mScrollableSuggestionsClip;
private ViewGroup mScrollableSuggestions;
private final Runnable mMoveScrollableSuggestionsToBg = () -> {
mScrollableSuggestionsClip.setZOrderedOnTop(false);
Toast.makeText(AutofillImeService.this, "Chips moved to bg - not clickable",
Toast.LENGTH_SHORT).show();
};
private final Runnable mMoveScrollableSuggestionsToFg = () -> {
mScrollableSuggestionsClip.setZOrderedOnTop(true);
Toast.makeText(AutofillImeService.this, "Chips moved to fg - clickable",
Toast.LENGTH_SHORT).show();
};
private final Runnable mMoveScrollableSuggestionsUp = () -> {
mSuggestionStrip.animate().translationY(-50).setDuration(500).start();
Toast.makeText(AutofillImeService.this, "Animating up",
Toast.LENGTH_SHORT).show();
};
private final Runnable mMoveScrollableSuggestionsDown = () -> {
mSuggestionStrip.animate().translationY(0).setDuration(500).start();
Toast.makeText(AutofillImeService.this, "Animating down",
Toast.LENGTH_SHORT).show();
};
@Override
public View onCreateInputView() {
@@ -61,10 +100,18 @@ public class AutofillImeService extends InputMethodService {
@Override
public void onStartInputView(EditorInfo info, boolean restarting) {
super.onStartInputView(info, restarting);
mInputView.removeAllViews();
Keyboard keyboard = Keyboard.qwerty(this);
mInputView.addView(keyboard.inflateKeyboardView(LayoutInflater.from(this), mInputView));
mSuggestionStrip = mInputView.findViewById(R.id.suggestion_view);
mSuggestionStrip = mInputView.findViewById(R.id.suggestion_strip);
mPinnedSuggestionsStart = mInputView.findViewById(R.id.pinned_suggestions_start);
mPinnedSuggestionsEnd = mInputView.findViewById(R.id.pinned_suggestions_end);
mScrollableSuggestionsClip = mInputView.findViewById(R.id.scrollable_suggestions_clip);
mScrollableSuggestions = mInputView.findViewById(R.id.scrollable_suggestions);
updateInlineSuggestionStrip(Collections.emptyList());
}
@Override
@@ -80,15 +127,6 @@ public class AutofillImeService extends InputMethodService {
private static final String TAG = "AutofillImeService";
private Handler mMainHandler = new Handler();
@GuardedBy("this")
private List<View> mSuggestionViews = new ArrayList<>();
@GuardedBy("this")
private List<Size> mSuggestionViewSizes = new ArrayList<>();
@GuardedBy("this")
private boolean mSuggestionViewVisible = false;
@Override
public InlineSuggestionsRequest onCreateInlineSuggestionsRequest(Bundle uiExtras) {
Log.d(TAG, "onCreateInlineSuggestionsRequest() called");
@@ -106,45 +144,65 @@ public class AutofillImeService extends InputMethodService {
@Override
public boolean onInlineSuggestionsResponse(InlineSuggestionsResponse response) {
Log.d(TAG, "onInlineSuggestionsResponse() called");
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
onInlineSuggestionsResponseInternal(response);
});
onInlineSuggestionsResponseInternal(response);
return true;
}
private synchronized void updateInlineSuggestionVisibility(boolean visible, boolean force) {
Log.d(TAG, "updateInlineSuggestionVisibility() called, visible=" + visible + ", force="
+ force);
mMainHandler.post(() -> {
Log.d(TAG, "updateInlineSuggestionVisibility() running");
if (visible == mSuggestionViewVisible && !force) {
return;
} else if (visible) {
mSuggestionStrip.removeAllViews();
final int size = mSuggestionViews.size();
for (int i = 0; i < size; i++) {
if(mSuggestionViews.get(i) == null) {
continue;
}
ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(
mSuggestionViewSizes.get(i).getWidth(),
mSuggestionViewSizes.get(i).getHeight());
mSuggestionStrip.addView(mSuggestionViews.get(i), layoutParams);
}
mSuggestionViewVisible = true;
} else {
mSuggestionStrip.removeAllViews();
mSuggestionViewVisible = false;
private void updateInlineSuggestionStrip(List<SuggestionItem> suggestionItems) {
mPinnedSuggestionsStart.removeAllViews();
mScrollableSuggestions.removeAllViews();
mPinnedSuggestionsEnd.removeAllViews();
final int size = suggestionItems.size();
if (size <= 0) {
mSuggestionStrip.setVisibility(View.GONE);
return;
}
// TODO: refactor me
mScrollableSuggestionsClip.setBackgroundColor(
getColor(R.color.suggestion_strip_background));
mSuggestionStrip.setVisibility(View.VISIBLE);
for (int i = 0; i < size; i++) {
final SuggestionItem suggestionItem = suggestionItems.get(i);
if (suggestionItem == null) {
continue;
}
});
final InlineContentView suggestionView = suggestionItem.mView;
if (suggestionItem.mIsPinned) {
if (mPinnedSuggestionsStart.getChildCount() <= 0) {
mPinnedSuggestionsStart.addView(suggestionView);
} else {
mPinnedSuggestionsEnd.addView(suggestionView);
}
} else {
mScrollableSuggestions.addView(suggestionView);
}
}
if (SHOWCASE_BG_FG_TRANSITION) {
rescheduleShowcaseBgFgTransitions();
}
if (SHOWCASE_UP_DOWN_TRANSITION) {
rescheduleShowcaseUpDownTransitions();
}
}
private synchronized void updateSuggestionViews(View[] suggestionViews, Size[] sizes) {
Log.d(TAG, "updateSuggestionViews() called");
mSuggestionViews = Arrays.asList(suggestionViews);
mSuggestionViewSizes = Arrays.asList(sizes);
final boolean visible = !mSuggestionViews.isEmpty();
updateInlineSuggestionVisibility(visible, true);
private void rescheduleShowcaseBgFgTransitions() {
final Handler handler = mInputView.getHandler();
handler.removeCallbacks(mMoveScrollableSuggestionsToBg);
handler.postDelayed(mMoveScrollableSuggestionsToBg, MOVE_SUGGESTIONS_TO_BG_TIMEOUT);
handler.removeCallbacks(mMoveScrollableSuggestionsToFg);
handler.postDelayed(mMoveScrollableSuggestionsToFg, MOVE_SUGGESTIONS_TO_FG_TIMEOUT);
}
private void rescheduleShowcaseUpDownTransitions() {
final Handler handler = mInputView.getHandler();
handler.removeCallbacks(mMoveScrollableSuggestionsUp);
handler.postDelayed(mMoveScrollableSuggestionsUp, MOVE_SUGGESTIONS_UP_TIMEOUT);
handler.removeCallbacks(mMoveScrollableSuggestionsDown);
handler.postDelayed(mMoveScrollableSuggestionsDown, MOVE_SUGGESTIONS_DOWN_TIMEOUT);
}
private void onInlineSuggestionsResponseInternal(InlineSuggestionsResponse response) {
@@ -152,38 +210,48 @@ public class AutofillImeService extends InputMethodService {
+ response.getInlineSuggestions().size());
final List<InlineSuggestion> inlineSuggestions = response.getInlineSuggestions();
final int totalSuggestionsCount = inlineSuggestions.size();
final AtomicInteger suggestionsCount = new AtomicInteger(totalSuggestionsCount);
final View[] suggestionViews = new View[totalSuggestionsCount];
final Size[] sizes = new Size[totalSuggestionsCount];
if (totalSuggestionsCount == 0) {
updateSuggestionViews(suggestionViews, sizes);
final int totalSuggestionsCount = inlineSuggestions.size();
if (totalSuggestionsCount <= 0) {
updateInlineSuggestionStrip(Collections.emptyList());
return;
}
for (int i=0; i<totalSuggestionsCount; i++) {
final Map<Integer, SuggestionItem> suggestionMap = Collections.synchronizedMap((
new TreeMap<>()));
final ExecutorService executor = Executors.newSingleThreadExecutor();
for (int i = 0; i < totalSuggestionsCount; i++) {
final int index = i;
InlineSuggestion inlineSuggestion = inlineSuggestions.get(index);
Size size = inlineSuggestion.getInfo().getInlinePresentationSpec().getMaxSize();
inlineSuggestion.inflate(this, size,
AsyncTask.THREAD_POOL_EXECUTOR,
suggestionView -> {
Log.d(TAG, "new inline suggestion view ready");
if(suggestionView != null) {
suggestionViews[index] = suggestionView;
sizes[index] = size;
suggestionView.setOnClickListener((v) -> {
Log.d(TAG, "Received click on the suggestion");
});
suggestionView.setOnLongClickListener((v) -> {
Log.d(TAG, "Received long click on the suggestion");
return true;
});
}
if (suggestionsCount.decrementAndGet() == 0) {
updateSuggestionViews(suggestionViews, sizes);
}
final InlineSuggestion inlineSuggestion = inlineSuggestions.get(i);
final Size size = inlineSuggestion.getInfo().getInlinePresentationSpec().getMaxSize();
inlineSuggestion.inflate(this, size, executor, suggestionView -> {
Log.d(TAG, "new inline suggestion view ready");
if(suggestionView != null) {
suggestionView.setLayoutParams(new ViewGroup.LayoutParams(
size.getWidth(), size.getHeight()));
suggestionView.setOnClickListener((v) -> {
Log.d(TAG, "Received click on the suggestion");
});
suggestionView.setOnLongClickListener((v) -> {
Log.d(TAG, "Received long click on the suggestion");
return true;
});
final SuggestionItem suggestionItem = new SuggestionItem(
suggestionView, /*isAction*/ inlineSuggestion.getInfo().isPinned());
suggestionMap.put(index, suggestionItem);
} else {
suggestionMap.put(index, null);
}
// Update the UI once the last inflation completed
if (suggestionMap.size() >= totalSuggestionsCount) {
final ArrayList<SuggestionItem> suggestionItems = new ArrayList<>(
suggestionMap.values());
getMainExecutor().execute(() -> updateInlineSuggestionStrip(suggestionItems));
}
});
}
}
@@ -191,4 +259,14 @@ public class AutofillImeService extends InputMethodService {
Log.d(TAG, "handle() called: [" + data + "]");
mDecoder.decodeAndApply(data);
}
static class SuggestionItem {
final InlineContentView mView;
final boolean mIsPinned;
SuggestionItem(InlineContentView view, boolean isPinned) {
mView = view;
mIsPinned = isPinned;
}
}
}

View File

@@ -0,0 +1,229 @@
/*
* Copyright 2020 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.autofillkeyboard;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.PixelFormat;
import android.os.Build;
import android.util.AttributeSet;
import android.view.Choreographer;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.inline.InlineContentView;
import android.widget.FrameLayout;
import androidx.annotation.AttrRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.collection.ArraySet;
/**
* This class is a container for showing {@link InlineContentView}s for cases
* where you want to ensure they appear only in a given area in your app. An
* example is having a scrollable list of items. Note that without this container
* the InlineContentViews' surfaces would cover parts of your app as these surfaces
* are owned by another process and always appearing on top of your app.
*/
@RequiresApi(api = Build.VERSION_CODES.R)
public class InlineContentClipView extends FrameLayout {
// The trick that we use here is to have a hidden SurfaceView to whose
// surface we reparent the surfaces of remote content views which are
// InlineContentViews. Since surface locations are based off the window
// top-left making, making one surface parent of another compounds the
// offset from the child's point of view. To compensate for that we
// apply transformation to the InlineContentViews.
@NonNull
private final ArraySet<InlineContentView> mReparentedDescendants = new ArraySet<>();
@NonNull
private final int[] mTempLocation = new int[2];
@NonNull
SurfaceView mSurfaceClipView;
private int mBackgroundColor;
public InlineContentClipView(@NonNull Context context) {
this(context, /*attrs*/ null);
}
public InlineContentClipView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, /*defStyleAttr*/ 0);
}
public InlineContentClipView(@NonNull Context context, @Nullable AttributeSet attrs,
@AttrRes int defStyleAttr) {
super(context, attrs, defStyleAttr);
mSurfaceClipView = new SurfaceView(context);
mSurfaceClipView.setZOrderOnTop(true);
mSurfaceClipView.getHolder().setFormat(PixelFormat.TRANSPARENT);
mSurfaceClipView.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
mSurfaceClipView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
drawBackgroundColorIfReady();
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width,
int height) { /*do nothing*/ }
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
updateState(InlineContentClipView.this, /*parentSurfaceProvider*/ null);
}
});
addView(mSurfaceClipView);
setWillNotDraw(false);
getViewTreeObserver().addOnPreDrawListener(() -> {
updateState(InlineContentClipView.this, mSurfaceClipView);
return true;
});
}
@Override
public void setBackgroundColor(int color) {
mBackgroundColor = color;
Choreographer.getInstance().postFrameCallback((frameTimeNanos) ->
drawBackgroundColorIfReady());
}
private void drawBackgroundColorIfReady() {
final Surface surface = mSurfaceClipView.getHolder().getSurface();
if (surface.isValid()) {
final Canvas canvas = surface.lockCanvas(null);
try {
canvas.drawColor(mBackgroundColor);
} finally {
surface.unlockCanvasAndPost(canvas);
}
}
}
/**
* Sets whether the surfaces of the {@link InlineContentView}s wrapped by this view
* should appear on top or behind this view's window. Normally, they are placed on top
* of the window, to allow interaction ith the embedded UI. Via this method, you can
* place the surface below the window. This means that all of the contents of the window
* this view is in will be visible on top of the {@link InlineContentView}s' surfaces.
*
* @param onTop Whether to show the surface on top of this view's window.
*
* @see InlineContentView
* @see InlineContentView#setZOrderedOnTop(boolean)
*/
public void setZOrderedOnTop(boolean onTop) {
mSurfaceClipView.setZOrderOnTop(onTop);
for (InlineContentView inlineContentView : mReparentedDescendants) {
inlineContentView.setZOrderedOnTop(onTop);
}
}
void updateState(@NonNull View root,
@Nullable SurfaceView parentSurfaceProvider) {
if (parentSurfaceProvider != null) {
mSurfaceClipView.getLocationInWindow(mTempLocation);
} else {
mTempLocation[0] = 0;
mTempLocation[1] = 0;
}
reparentChildSurfaceViewSurfacesRecursive(root, parentSurfaceProvider,
/*parentSurfaceLeft*/ mTempLocation[0], /*parentSurfaceTop*/ mTempLocation[1]);
}
private void reparentChildSurfaceViewSurfacesRecursive(@Nullable View root,
@Nullable SurfaceView parentSurfaceProvider, int parentSurfaceLeft,
int parentSurfaceTop) {
if (root == null || root == mSurfaceClipView) {
return;
}
if (root instanceof InlineContentView) {
// Surfaces of a surface view have a transformation matrix relative
// to the top-left of the window and when one is reparented to the
// other the transformation adds up and we need to compensate.
root.setTranslationX(-parentSurfaceLeft);
root.setTranslationY(-parentSurfaceTop);
final InlineContentView inlineContentView = (InlineContentView) root;
if (parentSurfaceProvider != null) {
if (mReparentedDescendants.contains(inlineContentView)) {
return;
}
inlineContentView.setSurfaceControlCallback(
new InlineContentView.SurfaceControlCallback() {
@Override
public void onCreated(SurfaceControl surfaceControl) {
// Our surface and its descendants are initially hidden until
// the descendants are reparented and their containers scrolled.
new SurfaceControl.Transaction()
.reparent(surfaceControl, parentSurfaceProvider.getSurfaceControl())
.apply();
}
@Override
public void onDestroyed(SurfaceControl surfaceControl) {
/* do nothing */
}
});
mReparentedDescendants.add(inlineContentView);
} else {
if (!mReparentedDescendants.contains(inlineContentView)) {
return;
}
// Unparent the surface control of the removed surface view.
final SurfaceControl surfaceControl = inlineContentView.getSurfaceControl();
if (surfaceControl != null && surfaceControl.isValid()) {
new SurfaceControl.Transaction()
.reparent(surfaceControl, /*newParent*/ null)
.apply();
}
mReparentedDescendants.remove(inlineContentView);
}
return;
}
if (root instanceof ViewGroup) {
final ViewGroup rootGroup = (ViewGroup) root;
final int childCount = rootGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = rootGroup.getChildAt(i);
reparentChildSurfaceViewSurfacesRecursive(child, parentSurfaceProvider,
parentSurfaceLeft, parentSurfaceTop);
}
}
}
}

View File

@@ -38,6 +38,7 @@ import android.service.autofill.SaveRequest;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Size;
import android.view.View;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillValue;
import android.widget.inline.InlinePresentationSpec;
@@ -78,7 +79,7 @@ public class InlineFillService extends AutofillService {
// Find autofillable fields
AssistStructure structure = getLatestAssistStructure(request);
ArrayMap<String, AutofillId> fields = getAutofillableFields(structure, request.getFlags());
ArrayMap<String, AutofillId> fields = getAutofillableFields(structure);
Log.d(TAG, "autofillable fields:" + fields);
if (fields.isEmpty()) {
@@ -176,15 +177,19 @@ public class InlineFillService extends AutofillService {
}
}
// if (inlineRequest != null) {
// // Reuse the first spec's height for the inline action size, as there isn't dedicated
// // value from the request for this.
// final int height = inlineRequest.getPresentationSpecs().get(0).getMinSize().getHeight();
// final Size actionIconSize = new Size(height, height);
// response.addDataset(
// newInlineActionDataset(context, actionIconSize, R.drawable.ic_settings,
// fields));
// }
if (inlineRequest != null) {
// Reuse the first spec's height for the inline action size, as there isn't dedicated
// value from the request for this.
final int height = inlineRequest.getInlinePresentationSpecs().get(0)
.getMinSize().getHeight();
final Size actionIconSize = new Size(height, height);
response.addDataset(
newInlineActionDataset(context, actionIconSize, R.drawable.ic_settings,
fields));
response.addDataset(
newInlineActionDataset(context, actionIconSize, R.drawable.ic_settings,
fields));
}
// 2.Add save info
Collection<AutofillId> ids = fields.values();
@@ -279,57 +284,50 @@ public class InlineFillService extends AutofillService {
* <p>An autofillable field is a {@link ViewNode} whose getHint(ViewNode) method.
*/
@NonNull
private ArrayMap<String, AutofillId> getAutofillableFields(@NonNull AssistStructure structure,
int flags) {
private ArrayMap<String, AutofillId> getAutofillableFields(@NonNull AssistStructure structure) {
ArrayMap<String, AutofillId> fields = new ArrayMap<>();
int nodes = structure.getWindowNodeCount();
for (int i = 0; i < nodes; i++) {
ViewNode node = structure.getWindowNodeAt(i).getRootViewNode();
addAutofillableFields(fields, node, flags);
addAutofillableFields(fields, node);
}
return fields;
ArrayMap<String, AutofillId> result = new ArrayMap<>();
int filedCount = fields.size();
for (int i = 0; i < filedCount; i++) {
String key = fields.keyAt(i);
AutofillId value = fields.valueAt(i);
// For fields with no hint we just use Field
if (key.equals(value.toString())) {
result.put("Field:" + i + "-", fields.valueAt(i));
} else {
result.put(key, fields.valueAt(i));
}
}
return result;
}
/**
* Adds any autofillable view from the {@link ViewNode} and its descendants to the map.
*/
private void addAutofillableFields(@NonNull Map<String, AutofillId> fields,
@NonNull ViewNode node, int flags) {
int type = node.getAutofillType();
String hint = getHint(node, flags);
if (hint != null) {
AutofillId id = node.getAutofillId();
if (!fields.containsKey(hint)) {
Log.v(TAG, "Setting hint " + hint + " on " + id);
fields.put(hint, id);
} else {
Log.v(TAG, "Ignoring hint " + hint + " on " + id
+ " because it was already set");
@NonNull ViewNode node) {
if (node.getAutofillType() == View.AUTOFILL_TYPE_TEXT) {
if (!fields.containsValue(node.getAutofillId())) {
final String key;
if (node.getHint() != null) {
key = node.getHint().toLowerCase();
} else {
key = node.getAutofillId().toString();
}
fields.put(key, node.getAutofillId());
}
}
int childrenSize = node.getChildCount();
for (int i = 0; i < childrenSize; i++) {
addAutofillableFields(fields, node.getChildAt(i), flags);
addAutofillableFields(fields, node.getChildAt(i));
}
}
/**
* Gets the autofill hint associated with the given node.
*
* <p>By default it just return the first entry on the node's
* {@link ViewNode#getAutofillHints() autofillHints} (when available), but subclasses could
* extend it to use heuristics when the app developer didn't explicitly provide these hints.
*/
@Nullable
protected String getHint(@NonNull ViewNode node, int flags) {
String[] hints = node.getAutofillHints();
if (hints == null) return null;
// We're simple, we only care about the first hint
String hint = hints[0].toLowerCase();
return hint;
}
/**
* Helper method to get the {@link AssistStructure} associated with the latest request
* in an autofill context.