diff options
10 files changed, 271 insertions, 35 deletions
diff --git a/core/api/system-current.txt b/core/api/system-current.txt index a145003b4bc6..1553214bc2eb 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -9212,6 +9212,7 @@ package android.service.autofill.augmented { method @NonNull public android.content.ComponentName getActivityComponent(); method @NonNull public android.view.autofill.AutofillId getFocusedId(); method @NonNull public android.view.autofill.AutofillValue getFocusedValue(); + method @Nullable public android.app.assist.AssistStructure.ViewNode getFocusedViewNode(); method @Nullable public android.view.inputmethod.InlineSuggestionsRequest getInlineSuggestionsRequest(); method @Nullable public android.service.autofill.augmented.PresentationParams getPresentationParams(); method public int getTaskId(); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 4ef24ff18c48..1086577b0a39 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1714,6 +1714,7 @@ package android.service.autofill.augmented { method @NonNull public android.content.ComponentName getActivityComponent(); method @NonNull public android.view.autofill.AutofillId getFocusedId(); method @NonNull public android.view.autofill.AutofillValue getFocusedValue(); + method @Nullable public android.app.assist.AssistStructure.ViewNode getFocusedViewNode(); method @Nullable public android.view.inputmethod.InlineSuggestionsRequest getInlineSuggestionsRequest(); method @Nullable public android.service.autofill.augmented.PresentationParams getPresentationParams(); method public int getTaskId(); @@ -2199,6 +2200,7 @@ package android.view.autofill { ctor public AutofillId(int, int); ctor public AutofillId(@NonNull android.view.autofill.AutofillId, long, int); method public boolean equalsIgnoreSession(@Nullable android.view.autofill.AutofillId); + method public boolean isNonVirtual(); method @NonNull public static android.view.autofill.AutofillId withoutSession(@NonNull android.view.autofill.AutofillId); } diff --git a/core/java/android/app/assist/AssistStructure.aidl b/core/java/android/app/assist/AssistStructure.aidl index ae0a34c92a0e..b997bbbe7d7c 100644 --- a/core/java/android/app/assist/AssistStructure.aidl +++ b/core/java/android/app/assist/AssistStructure.aidl @@ -17,3 +17,8 @@ package android.app.assist; parcelable AssistStructure; + +/** + * {@hide} + */ +parcelable AssistStructure.ViewNodeParcelable; diff --git a/core/java/android/app/assist/AssistStructure.java b/core/java/android/app/assist/AssistStructure.java index c15504cc0843..0f7fac4eefa5 100644 --- a/core/java/android/app/assist/AssistStructure.java +++ b/core/java/android/app/assist/AssistStructure.java @@ -271,7 +271,8 @@ public class AssistStructure implements Parcelable { + ", views=" + mNumWrittenViews + ", level=" + (mCurViewStackPos+levelAdj)); out.writeInt(VALIDATE_VIEW_TOKEN); - int flags = child.writeSelfToParcel(out, pwriter, mSanitizeOnWrite, mTmpMatrix); + int flags = child.writeSelfToParcel(out, pwriter, mSanitizeOnWrite, + mTmpMatrix, /*willWriteChildren=*/true); mNumWrittenViews++; // If the child has children, push it on the stack to write them next. if ((flags&ViewNode.FLAGS_HAS_CHILDREN) != 0) { @@ -724,11 +725,51 @@ public class AssistStructure implements Parcelable { public ViewNode() { } + ViewNode(@NonNull Parcel in) { + initializeFromParcelWithoutChildren(in, /*preader=*/null, /*tmpMatrix=*/null); + } + ViewNode(ParcelTransferReader reader, int nestingLevel) { final Parcel in = reader.readParcel(VALIDATE_VIEW_TOKEN, nestingLevel); reader.mNumReadViews++; - final PooledStringReader preader = reader.mStringReader; - mClassName = preader.readString(); + initializeFromParcelWithoutChildren(in, Objects.requireNonNull(reader.mStringReader), + Objects.requireNonNull(reader.mTmpMatrix)); + if ((mFlags & FLAGS_HAS_CHILDREN) != 0) { + final int numChildren = in.readInt(); + if (DEBUG_PARCEL_TREE || DEBUG_PARCEL_CHILDREN) { + Log.d(TAG, + "Preparing to read " + numChildren + + " children: @ #" + reader.mNumReadViews + + ", level " + nestingLevel); + } + mChildren = new ViewNode[numChildren]; + for (int i = 0; i < numChildren; i++) { + mChildren[i] = new ViewNode(reader, nestingLevel + 1); + } + } + } + + private static void writeString(@NonNull Parcel out, @Nullable PooledStringWriter pwriter, + @Nullable String str) { + if (pwriter != null) { + pwriter.writeString(str); + } else { + out.writeString(str); + } + } + + @Nullable + private static String readString(@NonNull Parcel in, @Nullable PooledStringReader preader) { + if (preader != null) { + return preader.readString(); + } + return in.readString(); + } + + // This does not read the child nodes. + void initializeFromParcelWithoutChildren(Parcel in, @Nullable PooledStringReader preader, + @Nullable float[] tmpMatrix) { + mClassName = readString(in, preader); mFlags = in.readInt(); final int flags = mFlags; mAutofillFlags = in.readInt(); @@ -736,10 +777,10 @@ public class AssistStructure implements Parcelable { if ((flags&FLAGS_HAS_ID) != 0) { mId = in.readInt(); if (mId != View.NO_ID) { - mIdEntry = preader.readString(); + mIdEntry = readString(in, preader); if (mIdEntry != null) { - mIdType = preader.readString(); - mIdPackage = preader.readString(); + mIdType = readString(in, preader); + mIdPackage = readString(in, preader); } } } @@ -784,10 +825,10 @@ public class AssistStructure implements Parcelable { mMaxLength = in.readInt(); } if ((autofillFlags & AUTOFILL_FLAGS_HAS_TEXT_ID_ENTRY) != 0) { - mTextIdEntry = preader.readString(); + mTextIdEntry = readString(in, preader); } if ((autofillFlags & AUTOFILL_FLAGS_HAS_HINT_ID_ENTRY) != 0) { - mHintIdEntry = preader.readString(); + mHintIdEntry = readString(in, preader); } } if ((flags&FLAGS_HAS_LARGE_COORDS) != 0) { @@ -809,8 +850,11 @@ public class AssistStructure implements Parcelable { } if ((flags&FLAGS_HAS_MATRIX) != 0) { mMatrix = new Matrix(); - in.readFloatArray(reader.mTmpMatrix); - mMatrix.setValues(reader.mTmpMatrix); + if (tmpMatrix == null) { + tmpMatrix = new float[9]; + } + in.readFloatArray(tmpMatrix); + mMatrix.setValues(tmpMatrix); } if ((flags&FLAGS_HAS_ELEVATION) != 0) { mElevation = in.readFloat(); @@ -839,21 +883,16 @@ public class AssistStructure implements Parcelable { if ((flags&FLAGS_HAS_EXTRAS) != 0) { mExtras = in.readBundle(); } - if ((flags&FLAGS_HAS_CHILDREN) != 0) { - final int NCHILDREN = in.readInt(); - if (DEBUG_PARCEL_TREE || DEBUG_PARCEL_CHILDREN) Log.d(TAG, - "Preparing to read " + NCHILDREN - + " children: @ #" + reader.mNumReadViews - + ", level " + nestingLevel); - mChildren = new ViewNode[NCHILDREN]; - for (int i=0; i<NCHILDREN; i++) { - mChildren[i] = new ViewNode(reader, nestingLevel + 1); - } - } } - int writeSelfToParcel(Parcel out, PooledStringWriter pwriter, boolean sanitizeOnWrite, - float[] tmpMatrix) { + /** + * This does not write the child nodes. + * + * @param willWriteChildren whether child nodes will be written to the parcel or not after + * calling this method. + */ + int writeSelfToParcel(@NonNull Parcel out, @Nullable PooledStringWriter pwriter, + boolean sanitizeOnWrite, @Nullable float[] tmpMatrix, boolean willWriteChildren) { // Guard used to skip non-sanitized data when writing for autofill. boolean writeSensitive = true; @@ -903,7 +942,7 @@ public class AssistStructure implements Parcelable { if (mExtras != null) { flags |= FLAGS_HAS_EXTRAS; } - if (mChildren != null) { + if (mChildren != null && willWriteChildren) { flags |= FLAGS_HAS_CHILDREN; } if (mAutofillId != null) { @@ -946,7 +985,7 @@ public class AssistStructure implements Parcelable { autofillFlags |= AUTOFILL_FLAGS_HAS_HINT_ID_ENTRY; } - pwriter.writeString(mClassName); + writeString(out, pwriter, mClassName); int writtenFlags = flags; if (autofillFlags != 0 && (mSanitized || !sanitizeOnWrite)) { @@ -966,10 +1005,10 @@ public class AssistStructure implements Parcelable { if ((flags&FLAGS_HAS_ID) != 0) { out.writeInt(mId); if (mId != View.NO_ID) { - pwriter.writeString(mIdEntry); + writeString(out, pwriter, mIdEntry); if (mIdEntry != null) { - pwriter.writeString(mIdType); - pwriter.writeString(mIdPackage); + writeString(out, pwriter, mIdType); + writeString(out, pwriter, mIdPackage); } } } @@ -1020,10 +1059,10 @@ public class AssistStructure implements Parcelable { out.writeInt(mMaxLength); } if ((autofillFlags & AUTOFILL_FLAGS_HAS_TEXT_ID_ENTRY) != 0) { - pwriter.writeString(mTextIdEntry); + writeString(out, pwriter, mTextIdEntry); } if ((autofillFlags & AUTOFILL_FLAGS_HAS_HINT_ID_ENTRY) != 0) { - pwriter.writeString(mHintIdEntry); + writeString(out, pwriter, mHintIdEntry); } } if ((flags&FLAGS_HAS_LARGE_COORDS) != 0) { @@ -1040,6 +1079,9 @@ public class AssistStructure implements Parcelable { out.writeInt(mScrollY); } if ((flags&FLAGS_HAS_MATRIX) != 0) { + if (tmpMatrix == null) { + tmpMatrix = new float[9]; + } mMatrix.getValues(tmpMatrix); out.writeFloatArray(tmpMatrix); } @@ -1695,6 +1737,57 @@ public class AssistStructure implements Parcelable { } /** + * A parcelable wrapper class around {@link ViewNode}. + * + * <p>This class, when parceled and unparceled, does not carry the child nodes. + * + * @hide + */ + public static final class ViewNodeParcelable implements Parcelable { + + @NonNull + private final ViewNode mViewNode; + + public ViewNodeParcelable(@NonNull ViewNode viewNode) { + mViewNode = viewNode; + } + + public ViewNodeParcelable(@NonNull Parcel in) { + mViewNode = new ViewNode(in); + } + + @NonNull + public ViewNode getViewNode() { + return mViewNode; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel parcel, int flags) { + mViewNode.writeSelfToParcel(parcel, /*pwriter=*/null, /*sanitizeOnWrite=*/false, + /*tmpMatrix*/null, /*willWriteChildren=*/ false); + } + + @NonNull + public static final Parcelable.Creator<ViewNodeParcelable> CREATOR = + new Parcelable.Creator<ViewNodeParcelable>() { + @Override + public ViewNodeParcelable createFromParcel(@NonNull Parcel in) { + return new ViewNodeParcelable(in); + } + + @Override + public ViewNodeParcelable[] newArray(int size) { + return new ViewNodeParcelable[size]; + } + }; + } + + /** * POJO used to override some autofill-related values when the node is parcelized. * * @hide @@ -1704,17 +1797,35 @@ public class AssistStructure implements Parcelable { public AutofillValue value; } - static class ViewNodeBuilder extends ViewStructure { + /** + * @hide + */ + public static class ViewNodeBuilder extends ViewStructure { final AssistStructure mAssist; final ViewNode mNode; final boolean mAsync; + /** + * Used to instantiate a builder for a stand-alone {@link ViewNode} which is not associated + * to a properly created {@link AssistStructure}. + */ + public ViewNodeBuilder() { + mAssist = new AssistStructure(); + mNode = new ViewNode(); + mAsync = false; + } + ViewNodeBuilder(AssistStructure assist, ViewNode node, boolean async) { mAssist = assist; mNode = node; mAsync = async; } + @NonNull + public ViewNode getViewNode() { + return mNode; + } + @Override public void setId(int id, String packageName, String typeName, String entryName) { mNode.mId = id; diff --git a/core/java/android/service/autofill/augmented/AugmentedAutofillService.java b/core/java/android/service/autofill/augmented/AugmentedAutofillService.java index b34c2dceeee8..aeeaa97e2287 100644 --- a/core/java/android/service/autofill/augmented/AugmentedAutofillService.java +++ b/core/java/android/service/autofill/augmented/AugmentedAutofillService.java @@ -26,6 +26,8 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.app.Service; +import android.app.assist.AssistStructure.ViewNode; +import android.app.assist.AssistStructure.ViewNodeParcelable; import android.content.ComponentName; import android.content.Intent; import android.graphics.Rect; @@ -415,6 +417,8 @@ public abstract class AugmentedAutofillService extends Service { @GuardedBy("mLock") private AutofillValue mFocusedValue; @GuardedBy("mLock") + private ViewNode mFocusedViewNode; + @GuardedBy("mLock") private IFillCallback mCallback; /** @@ -532,6 +536,7 @@ public abstract class AugmentedAutofillService extends Service { synchronized (mLock) { mFocusedId = focusedId; mFocusedValue = focusedValue; + mFocusedViewNode = null; if (mCallback != null) { try { if (!mCallback.isCompleted()) { @@ -570,6 +575,25 @@ public abstract class AugmentedAutofillService extends Service { } } + @Nullable + public ViewNode getFocusedViewNode() { + synchronized (mLock) { + if (mFocusedViewNode == null) { + try { + final ViewNodeParcelable viewNodeParcelable = mClient.getViewNodeParcelable( + mFocusedId); + if (viewNodeParcelable != null) { + mFocusedViewNode = viewNodeParcelable.getViewNode(); + } + } catch (RemoteException e) { + Log.e(TAG, "Error getting the ViewNode of the focused view: " + e); + return null; + } + } + return mFocusedViewNode; + } + } + void logEvent(@ReportEvent int event) { if (sVerbose) Log.v(TAG, "returnAndLogResult(): " + event); long duration = -1; diff --git a/core/java/android/service/autofill/augmented/FillRequest.java b/core/java/android/service/autofill/augmented/FillRequest.java index 6927cf6541e0..f7f721a60aa0 100644 --- a/core/java/android/service/autofill/augmented/FillRequest.java +++ b/core/java/android/service/autofill/augmented/FillRequest.java @@ -19,6 +19,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.annotation.TestApi; +import android.app.assist.AssistStructure.ViewNode; import android.content.ComponentName; import android.service.autofill.augmented.AugmentedAutofillService.AutofillProxy; import android.view.autofill.AutofillId; @@ -81,6 +82,14 @@ public final class FillRequest { } /** + * Gets the current {@link ViewNode} information of the field that triggered the request. + */ + @Nullable + public ViewNode getFocusedViewNode() { + return mProxy.getFocusedViewNode(); + } + + /** * Gets the Smart Suggestions object used to embed the autofill UI. * * @return object used to embed the autofill UI, or {@code null} if not supported. @@ -98,7 +107,7 @@ public final class FillRequest { - // Code below generated by codegen v1.0.14. + // Code below generated by codegen v1.0.22. // // DO NOT MODIFY! // CHECKSTYLE:OFF Generated code @@ -151,10 +160,10 @@ public final class FillRequest { } @DataClass.Generated( - time = 1577399314707L, - codegenVersion = "1.0.14", + time = 1608160139217L, + codegenVersion = "1.0.22", sourceFile = "frameworks/base/core/java/android/service/autofill/augmented/FillRequest.java", - inputSignatures = "private final @android.annotation.NonNull android.service.autofill.augmented.AugmentedAutofillService.AutofillProxy mProxy\nprivate final @android.annotation.Nullable android.view.inputmethod.InlineSuggestionsRequest mInlineSuggestionsRequest\npublic int getTaskId()\npublic @android.annotation.NonNull android.content.ComponentName getActivityComponent()\npublic @android.annotation.NonNull android.view.autofill.AutofillId getFocusedId()\npublic @android.annotation.NonNull android.view.autofill.AutofillValue getFocusedValue()\npublic @android.annotation.Nullable android.service.autofill.augmented.PresentationParams getPresentationParams()\n java.lang.String proxyToString()\nclass FillRequest extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genToString=true, genBuilder=false, genHiddenConstructor=true)") + inputSignatures = "private final @android.annotation.NonNull android.service.autofill.augmented.AugmentedAutofillService.AutofillProxy mProxy\nprivate final @android.annotation.Nullable android.view.inputmethod.InlineSuggestionsRequest mInlineSuggestionsRequest\npublic int getTaskId()\npublic @android.annotation.NonNull android.content.ComponentName getActivityComponent()\npublic @android.annotation.NonNull android.view.autofill.AutofillId getFocusedId()\npublic @android.annotation.NonNull android.view.autofill.AutofillValue getFocusedValue()\npublic @android.annotation.Nullable android.app.assist.AssistStructure.ViewNode getFocusedViewNode()\npublic @android.annotation.Nullable android.service.autofill.augmented.PresentationParams getPresentationParams()\n java.lang.String proxyToString()\nclass FillRequest extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genToString=true, genBuilder=false, genHiddenConstructor=true)") @Deprecated private void __metadata() {} diff --git a/core/java/android/view/autofill/AutofillId.java b/core/java/android/view/autofill/AutofillId.java index 82d52b67c06a..ae145de21190 100644 --- a/core/java/android/view/autofill/AutofillId.java +++ b/core/java/android/view/autofill/AutofillId.java @@ -143,6 +143,7 @@ public final class AutofillId implements Parcelable { * * @hide */ + @TestApi public boolean isNonVirtual() { return !isVirtualInt() && !isVirtualLong(); } diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java index 364ae8186e54..794181e388cf 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -32,6 +32,9 @@ import android.annotation.RequiresFeature; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; +import android.app.assist.AssistStructure.ViewNode; +import android.app.assist.AssistStructure.ViewNodeBuilder; +import android.app.assist.AssistStructure.ViewNodeParcelable; import android.content.AutofillOptions; import android.content.ClipData; import android.content.ComponentName; @@ -64,6 +67,8 @@ import android.view.Choreographer; import android.view.ContentInfo; import android.view.KeyEvent; import android.view.View; +import android.view.ViewRootImpl; +import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; @@ -3581,6 +3586,35 @@ public final class AutofillManager { mAfm = new WeakReference<>(autofillManager); } + @Nullable + @Override + public ViewNodeParcelable getViewNodeParcelable(@NonNull AutofillId id) { + final AutofillManager afm = mAfm.get(); + if (afm == null) return null; + + final View view = getView(afm, id); + if (view == null) { + Log.w(TAG, "getViewNodeParcelable(" + id + "): could not find view"); + return null; + } + final ViewRootImpl root = view.getViewRootImpl(); + if (root != null + && (root.getWindowFlags() & WindowManager.LayoutParams.FLAG_SECURE) == 0) { + ViewNodeBuilder viewStructure = new ViewNodeBuilder(); + viewStructure.setAutofillId(view.getAutofillId()); + view.onProvideAutofillStructure(viewStructure, /* flags= */ 0); + // TODO(b/141703532): We don't call View#onProvideAutofillVirtualStructure for + // efficiency reason. But this also means we will return null for virtual views + // for now. We will add a new API to fetch the view node info of the virtual + // child view. + ViewNode viewNode = viewStructure.getViewNode(); + if (viewNode != null && id.equals(viewNode.getAutofillId())) { + return new ViewNodeParcelable(viewNode); + } + } + return null; + } + @Override public Rect getViewCoordinates(@NonNull AutofillId id) { final AutofillManager afm = mAfm.get(); diff --git a/core/java/android/view/autofill/IAugmentedAutofillManagerClient.aidl b/core/java/android/view/autofill/IAugmentedAutofillManagerClient.aidl index 8526c1e443c8..7d08bcf34398 100644 --- a/core/java/android/view/autofill/IAugmentedAutofillManagerClient.aidl +++ b/core/java/android/view/autofill/IAugmentedAutofillManagerClient.aidl @@ -18,6 +18,7 @@ package android.view.autofill; import java.util.List; +import android.app.assist.AssistStructure; import android.graphics.Rect; import android.view.autofill.AutofillId; import android.view.autofill.AutofillValue; @@ -36,6 +37,11 @@ interface IAugmentedAutofillManagerClient { Rect getViewCoordinates(in AutofillId id); /** + * Gets the autofill view structure of the input field view. + */ + AssistStructure.ViewNodeParcelable getViewNodeParcelable(in AutofillId id); + + /** * Autofills the activity with the contents of the values. */ void autofill(int sessionId, in List<AutofillId> ids, in List<AutofillValue> values, diff --git a/core/tests/coretests/src/android/app/assist/AssistStructureTest.java b/core/tests/coretests/src/android/app/assist/AssistStructureTest.java index da386a6bac90..4609c23bbbd1 100644 --- a/core/tests/coretests/src/android/app/assist/AssistStructureTest.java +++ b/core/tests/coretests/src/android/app/assist/AssistStructureTest.java @@ -23,11 +23,14 @@ import static android.view.View.IMPORTANT_FOR_AUTOFILL_YES; import static com.google.common.truth.Truth.assertThat; import android.app.assist.AssistStructure.ViewNode; +import android.app.assist.AssistStructure.ViewNodeBuilder; +import android.app.assist.AssistStructure.ViewNodeParcelable; import android.content.Context; import android.os.Parcel; import android.os.SystemClock; import android.text.InputFilter; import android.util.Log; +import android.view.View; import android.view.autofill.AutofillId; import android.widget.EditText; import android.widget.FrameLayout; @@ -219,6 +222,28 @@ public class AssistStructureTest { } } + @Test + public void testViewNodeParcelableForAutofill() { + Log.d(TAG, "Adding view with " + BIG_VIEW_SIZE + " chars"); + + View view = newBigView(); + mActivity.addView(view); + waitUntilViewsAreLaidOff(); + + assertThat(view.getViewRootImpl()).isNotNull(); + ViewNodeBuilder viewStructure = new ViewNodeBuilder(); + viewStructure.setAutofillId(view.getAutofillId()); + view.onProvideAutofillStructure(viewStructure, /* flags= */ 0); + ViewNodeParcelable viewNodeParcelable = new ViewNodeParcelable(viewStructure.getViewNode()); + + // Check properties on "original" view node. + assertBigView(viewNodeParcelable.getViewNode()); + + // Check properties on "cloned" view node. + ViewNodeParcelable clone = cloneThroughParcel(viewNodeParcelable); + assertBigView(clone.getViewNode()); + } + private EditText newSmallView() { EditText view = new EditText(mContext); view.setText("I AM GROOT"); @@ -272,6 +297,24 @@ public class AssistStructureTest { assertThat(hint.charAt(BIG_VIEW_SIZE - 1)).isEqualTo(BIG_VIEW_CHAR); } + private ViewNodeParcelable cloneThroughParcel(ViewNodeParcelable viewNodeParcelable) { + Parcel parcel = Parcel.obtain(); + + try { + // Write to parcel + parcel.setDataPosition(0); // Validity Check + viewNodeParcelable.writeToParcel(parcel, NO_FLAGS); + + // Read from parcel + parcel.setDataPosition(0); + ViewNodeParcelable clone = ViewNodeParcelable.CREATOR.createFromParcel(parcel); + assertThat(clone).isNotNull(); + return clone; + } finally { + parcel.recycle(); + } + } + private AssistStructure cloneThroughParcel(AssistStructure structure) { Parcel parcel = Parcel.obtain(); |