Merge "New Autofill API: TYPE_CONTEXT_COMMITTED events."
This commit is contained in:
@@ -37283,10 +37283,15 @@ package android.service.autofill {
|
||||
}
|
||||
|
||||
public static final class FillEventHistory.Event {
|
||||
method public java.util.Map<android.view.autofill.AutofillId, java.lang.String> getChangedFields();
|
||||
method public android.os.Bundle getClientState();
|
||||
method public java.lang.String getDatasetId();
|
||||
method public java.util.Set<java.lang.String> getIgnoredDatasetIds();
|
||||
method public java.util.Map<android.view.autofill.AutofillId, java.util.Set<java.lang.String>> getManuallyEnteredField();
|
||||
method public java.util.Set<java.lang.String> getSelectedDatasetIds();
|
||||
method public int getType();
|
||||
field public static final int TYPE_AUTHENTICATION_SELECTED = 2; // 0x2
|
||||
field public static final int TYPE_CONTEXT_COMMITTED = 4; // 0x4
|
||||
field public static final int TYPE_DATASET_AUTHENTICATION_SELECTED = 1; // 0x1
|
||||
field public static final int TYPE_DATASET_SELECTED = 0; // 0x0
|
||||
field public static final int TYPE_SAVE_SHOWN = 3; // 0x3
|
||||
@@ -37307,6 +37312,7 @@ package android.service.autofill {
|
||||
method public int describeContents();
|
||||
method public void writeToParcel(android.os.Parcel, int);
|
||||
field public static final android.os.Parcelable.Creator<android.service.autofill.FillResponse> CREATOR;
|
||||
field public static final int FLAG_TRACK_CONTEXT_COMMITED = 1; // 0x1
|
||||
}
|
||||
|
||||
public static final class FillResponse.Builder {
|
||||
@@ -37315,6 +37321,7 @@ package android.service.autofill {
|
||||
method public android.service.autofill.FillResponse build();
|
||||
method public android.service.autofill.FillResponse.Builder setAuthentication(android.view.autofill.AutofillId[], android.content.IntentSender, android.widget.RemoteViews);
|
||||
method public android.service.autofill.FillResponse.Builder setClientState(android.os.Bundle);
|
||||
method public android.service.autofill.FillResponse.Builder setFlags(int);
|
||||
method public android.service.autofill.FillResponse.Builder setIgnoredIds(android.view.autofill.AutofillId...);
|
||||
method public android.service.autofill.FillResponse.Builder setSaveInfo(android.service.autofill.SaveInfo);
|
||||
}
|
||||
@@ -52062,6 +52069,7 @@ package android.widget {
|
||||
method public android.graphics.Typeface getTypeface();
|
||||
method public android.text.style.URLSpan[] getUrls();
|
||||
method public boolean hasSelection();
|
||||
method public void invalidate(int, int, int, int);
|
||||
method public boolean isAllCaps();
|
||||
method public boolean isCursorVisible();
|
||||
method public boolean isElegantTextHeight();
|
||||
|
||||
@@ -40385,10 +40385,15 @@ package android.service.autofill {
|
||||
}
|
||||
|
||||
public static final class FillEventHistory.Event {
|
||||
method public java.util.Map<android.view.autofill.AutofillId, java.lang.String> getChangedFields();
|
||||
method public android.os.Bundle getClientState();
|
||||
method public java.lang.String getDatasetId();
|
||||
method public java.util.Set<java.lang.String> getIgnoredDatasetIds();
|
||||
method public java.util.Map<android.view.autofill.AutofillId, java.util.Set<java.lang.String>> getManuallyEnteredField();
|
||||
method public java.util.Set<java.lang.String> getSelectedDatasetIds();
|
||||
method public int getType();
|
||||
field public static final int TYPE_AUTHENTICATION_SELECTED = 2; // 0x2
|
||||
field public static final int TYPE_CONTEXT_COMMITTED = 4; // 0x4
|
||||
field public static final int TYPE_DATASET_AUTHENTICATION_SELECTED = 1; // 0x1
|
||||
field public static final int TYPE_DATASET_SELECTED = 0; // 0x0
|
||||
field public static final int TYPE_SAVE_SHOWN = 3; // 0x3
|
||||
@@ -40409,6 +40414,7 @@ package android.service.autofill {
|
||||
method public int describeContents();
|
||||
method public void writeToParcel(android.os.Parcel, int);
|
||||
field public static final android.os.Parcelable.Creator<android.service.autofill.FillResponse> CREATOR;
|
||||
field public static final int FLAG_TRACK_CONTEXT_COMMITED = 1; // 0x1
|
||||
}
|
||||
|
||||
public static final class FillResponse.Builder {
|
||||
@@ -40417,6 +40423,7 @@ package android.service.autofill {
|
||||
method public android.service.autofill.FillResponse build();
|
||||
method public android.service.autofill.FillResponse.Builder setAuthentication(android.view.autofill.AutofillId[], android.content.IntentSender, android.widget.RemoteViews);
|
||||
method public android.service.autofill.FillResponse.Builder setClientState(android.os.Bundle);
|
||||
method public android.service.autofill.FillResponse.Builder setFlags(int);
|
||||
method public android.service.autofill.FillResponse.Builder setIgnoredIds(android.view.autofill.AutofillId...);
|
||||
method public android.service.autofill.FillResponse.Builder setSaveInfo(android.service.autofill.SaveInfo);
|
||||
}
|
||||
@@ -56165,6 +56172,7 @@ package android.widget {
|
||||
method public android.graphics.Typeface getTypeface();
|
||||
method public android.text.style.URLSpan[] getUrls();
|
||||
method public boolean hasSelection();
|
||||
method public void invalidate(int, int, int, int);
|
||||
method public boolean isAllCaps();
|
||||
method public boolean isCursorVisible();
|
||||
method public boolean isElegantTextHeight();
|
||||
|
||||
@@ -37575,10 +37575,15 @@ package android.service.autofill {
|
||||
}
|
||||
|
||||
public static final class FillEventHistory.Event {
|
||||
method public java.util.Map<android.view.autofill.AutofillId, java.lang.String> getChangedFields();
|
||||
method public android.os.Bundle getClientState();
|
||||
method public java.lang.String getDatasetId();
|
||||
method public java.util.Set<java.lang.String> getIgnoredDatasetIds();
|
||||
method public java.util.Map<android.view.autofill.AutofillId, java.util.Set<java.lang.String>> getManuallyEnteredField();
|
||||
method public java.util.Set<java.lang.String> getSelectedDatasetIds();
|
||||
method public int getType();
|
||||
field public static final int TYPE_AUTHENTICATION_SELECTED = 2; // 0x2
|
||||
field public static final int TYPE_CONTEXT_COMMITTED = 4; // 0x4
|
||||
field public static final int TYPE_DATASET_AUTHENTICATION_SELECTED = 1; // 0x1
|
||||
field public static final int TYPE_DATASET_SELECTED = 0; // 0x0
|
||||
field public static final int TYPE_SAVE_SHOWN = 3; // 0x3
|
||||
@@ -37599,6 +37604,7 @@ package android.service.autofill {
|
||||
method public int describeContents();
|
||||
method public void writeToParcel(android.os.Parcel, int);
|
||||
field public static final android.os.Parcelable.Creator<android.service.autofill.FillResponse> CREATOR;
|
||||
field public static final int FLAG_TRACK_CONTEXT_COMMITED = 1; // 0x1
|
||||
}
|
||||
|
||||
public static final class FillResponse.Builder {
|
||||
@@ -37607,6 +37613,7 @@ package android.service.autofill {
|
||||
method public android.service.autofill.FillResponse build();
|
||||
method public android.service.autofill.FillResponse.Builder setAuthentication(android.view.autofill.AutofillId[], android.content.IntentSender, android.widget.RemoteViews);
|
||||
method public android.service.autofill.FillResponse.Builder setClientState(android.os.Bundle);
|
||||
method public android.service.autofill.FillResponse.Builder setFlags(int);
|
||||
method public android.service.autofill.FillResponse.Builder setIgnoredIds(android.view.autofill.AutofillId...);
|
||||
method public android.service.autofill.FillResponse.Builder setSaveInfo(android.service.autofill.SaveInfo);
|
||||
}
|
||||
@@ -52670,6 +52677,7 @@ package android.widget {
|
||||
method public android.graphics.Typeface getTypeface();
|
||||
method public android.text.style.URLSpan[] getUrls();
|
||||
method public boolean hasSelection();
|
||||
method public void invalidate(int, int, int, int);
|
||||
method public boolean isAllCaps();
|
||||
method public boolean isCursorVisible();
|
||||
method public boolean isElegantTextHeight();
|
||||
|
||||
@@ -17,19 +17,27 @@
|
||||
package android.service.autofill;
|
||||
|
||||
import android.annotation.IntDef;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.content.IntentSender;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.ArraySet;
|
||||
import android.view.autofill.AutofillId;
|
||||
import android.view.autofill.AutofillManager;
|
||||
|
||||
import com.android.internal.util.ArrayUtils;
|
||||
import com.android.internal.util.Preconditions;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Describes what happened after the last
|
||||
@@ -136,9 +144,21 @@ public final class FillEventHistory implements Parcelable {
|
||||
int numEvents = mEvents.size();
|
||||
for (int i = 0; i < numEvents; i++) {
|
||||
Event event = mEvents.get(i);
|
||||
dest.writeInt(event.getType());
|
||||
dest.writeString(event.getDatasetId());
|
||||
dest.writeBundle(event.getClientState());
|
||||
dest.writeInt(event.mEventType);
|
||||
dest.writeString(event.mDatasetId);
|
||||
dest.writeBundle(event.mClientState);
|
||||
dest.writeStringList(event.mSelectedDatasetIds);
|
||||
dest.writeArraySet(event.mIgnoredDatasetIds);
|
||||
dest.writeTypedList(event.mChangedFieldIds);
|
||||
dest.writeStringList(event.mChangedDatasetIds);
|
||||
|
||||
dest.writeTypedList(event.mManuallyFilledFieldIds);
|
||||
if (event.mManuallyFilledFieldIds != null) {
|
||||
final int size = event.mManuallyFilledFieldIds.size();
|
||||
for (int j = 0; j < size; j++) {
|
||||
dest.writeStringList(event.mManuallyFilledDatasetIds.get(j));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,12 +196,40 @@ public final class FillEventHistory implements Parcelable {
|
||||
/** A save UI was shown. */
|
||||
public static final int TYPE_SAVE_SHOWN = 3;
|
||||
|
||||
/**
|
||||
* A committed autofill context for which the autofill service provided datasets.
|
||||
*
|
||||
* <p>This event is useful to track:
|
||||
* <ul>
|
||||
* <li>Which datasets (if any) were selected by the user
|
||||
* ({@link #getSelectedDatasetIds()}).
|
||||
* <li>Which datasets (if any) were NOT selected by the user
|
||||
* ({@link #getIgnoredDatasetIds()}).
|
||||
* <li>Which fields in the selected datasets were changed by the user after the dataset
|
||||
* was selected ({@link #getChangedFields()}.
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>Note: </b>This event is only generated when:
|
||||
* <ul>
|
||||
* <li>The autofill context is committed.
|
||||
* <li>The service provides at least one dataset in the
|
||||
* {@link FillResponse fill responses} associated with the context.
|
||||
* <li>The last {@link FillResponse fill responses} associated with the context has the
|
||||
* {@link FillResponse#FLAG_TRACK_CONTEXT_COMMITED} flag.
|
||||
* </ul>
|
||||
*
|
||||
* <p>See {@link android.view.autofill.AutofillManager} for more information about autofill
|
||||
* contexts.
|
||||
*/
|
||||
public static final int TYPE_CONTEXT_COMMITTED = 4;
|
||||
|
||||
/** @hide */
|
||||
@IntDef(
|
||||
value = {TYPE_DATASET_SELECTED,
|
||||
TYPE_DATASET_AUTHENTICATION_SELECTED,
|
||||
TYPE_AUTHENTICATION_SELECTED,
|
||||
TYPE_SAVE_SHOWN})
|
||||
TYPE_SAVE_SHOWN,
|
||||
TYPE_CONTEXT_COMMITTED})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@interface EventIds{}
|
||||
|
||||
@@ -189,6 +237,17 @@ public final class FillEventHistory implements Parcelable {
|
||||
@Nullable private final String mDatasetId;
|
||||
@Nullable private final Bundle mClientState;
|
||||
|
||||
// Note: mSelectedDatasetIds is stored as List<> instead of Set because Session already
|
||||
// stores it as List
|
||||
@Nullable private final List<String> mSelectedDatasetIds;
|
||||
@Nullable private final ArraySet<String> mIgnoredDatasetIds;
|
||||
|
||||
@Nullable private final ArrayList<AutofillId> mChangedFieldIds;
|
||||
@Nullable private final ArrayList<String> mChangedDatasetIds;
|
||||
|
||||
@Nullable private final ArrayList<AutofillId> mManuallyFilledFieldIds;
|
||||
@Nullable private final ArrayList<ArrayList<String>> mManuallyFilledDatasetIds;
|
||||
|
||||
/**
|
||||
* Returns the type of the event.
|
||||
*
|
||||
@@ -219,6 +278,137 @@ public final class FillEventHistory implements Parcelable {
|
||||
return mClientState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns which datasets were selected by the user.
|
||||
*
|
||||
* <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}.
|
||||
*/
|
||||
@NonNull public Set<String> getSelectedDatasetIds() {
|
||||
return mSelectedDatasetIds == null ? Collections.emptySet()
|
||||
: new ArraySet<>(mSelectedDatasetIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns which datasets were NOT selected by the user.
|
||||
*
|
||||
* <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}.
|
||||
*/
|
||||
@NonNull public Set<String> getIgnoredDatasetIds() {
|
||||
return mIgnoredDatasetIds == null ? Collections.emptySet() : mIgnoredDatasetIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns which fields in the selected datasets were changed by the user after the dataset
|
||||
* was selected.
|
||||
*
|
||||
* <p>For example, server provides:
|
||||
*
|
||||
* <pre class="prettyprint">
|
||||
* FillResponse response = new FillResponse.Builder()
|
||||
* .addDataset(new Dataset.Builder(presentation1)
|
||||
* .setId("4815")
|
||||
* .setValue(usernameId, AutofillValue.forText("MrPlow"))
|
||||
* .build())
|
||||
* .addDataset(new Dataset.Builder(presentation2)
|
||||
* .setId("162342")
|
||||
* .setValue(passwordId, AutofillValue.forText("D'OH"))
|
||||
* .build())
|
||||
* .build();
|
||||
* </pre>
|
||||
*
|
||||
* <p>User select both datasets (for username and password) but after the fields are
|
||||
* autofilled, user changes them to:
|
||||
*
|
||||
* <pre class="prettyprint">
|
||||
* username = "ElBarto";
|
||||
* password = "AyCaramba";
|
||||
* </pre>
|
||||
*
|
||||
* <p>Then the result is the following map:
|
||||
*
|
||||
* <pre class="prettyprint">
|
||||
* usernameId => "4815"
|
||||
* passwordId => "162342"
|
||||
* </pre>
|
||||
*
|
||||
* <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}.
|
||||
*
|
||||
* @return map map whose key is the id of the change fields, and value is the id of
|
||||
* dataset that has that field and was selected by the user.
|
||||
*/
|
||||
@NonNull public Map<AutofillId, String> getChangedFields() {
|
||||
if (mChangedFieldIds == null || mChangedDatasetIds == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
final int size = mChangedFieldIds.size();
|
||||
final ArrayMap<AutofillId, String> changedFields = new ArrayMap<>(size);
|
||||
for (int i = 0; i < size; i++) {
|
||||
changedFields.put(mChangedFieldIds.get(i), mChangedDatasetIds.get(i));
|
||||
}
|
||||
return changedFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns which fields were available on datasets provided by the service but manually
|
||||
* entered by the user.
|
||||
*
|
||||
* <p>For example, server provides:
|
||||
*
|
||||
* <pre class="prettyprint">
|
||||
* FillResponse response = new FillResponse.Builder()
|
||||
* .addDataset(new Dataset.Builder(presentation1)
|
||||
* .setId("4815")
|
||||
* .setValue(usernameId, AutofillValue.forText("MrPlow"))
|
||||
* .setValue(passwordId, AutofillValue.forText("AyCaramba"))
|
||||
* .build())
|
||||
* .addDataset(new Dataset.Builder(presentation2)
|
||||
* .setId("162342")
|
||||
* .setValue(usernameId, AutofillValue.forText("ElBarto"))
|
||||
* .setValue(passwordId, AutofillValue.forText("D'OH"))
|
||||
* .build())
|
||||
* .addDataset(new Dataset.Builder(presentation3)
|
||||
* .setId("108")
|
||||
* .setValue(usernameId, AutofillValue.forText("MrPlow"))
|
||||
* .setValue(passwordId, AutofillValue.forText("D'OH"))
|
||||
* .build())
|
||||
* .build();
|
||||
* </pre>
|
||||
*
|
||||
* <p>User doesn't select a dataset but manually enters:
|
||||
*
|
||||
* <pre class="prettyprint">
|
||||
* username = "MrPlow";
|
||||
* password = "D'OH";
|
||||
* </pre>
|
||||
*
|
||||
* <p>Then the result is the following map:
|
||||
*
|
||||
* <pre class="prettyprint">
|
||||
* usernameId => { "4815", "108"}
|
||||
* passwordId => { "162342", "108" }
|
||||
* </pre>
|
||||
*
|
||||
* <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}.
|
||||
*
|
||||
* @return map map whose key is the id of the manually-entered field, and value is the
|
||||
* ids of the datasets that have that value but were not selected by the user.
|
||||
*/
|
||||
@Nullable public Map<AutofillId, Set<String>> getManuallyEnteredField() {
|
||||
if (mManuallyFilledFieldIds == null || mManuallyFilledDatasetIds == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
final int size = mManuallyFilledFieldIds.size();
|
||||
final Map<AutofillId, Set<String>> manuallyFilledFields = new ArrayMap<>(size);
|
||||
for (int i = 0; i < size; i++) {
|
||||
final AutofillId fieldId = mManuallyFilledFieldIds.get(i);
|
||||
final ArrayList<String> datasetIds = mManuallyFilledDatasetIds.get(i);
|
||||
manuallyFilledFields.put(fieldId, new ArraySet<>(datasetIds));
|
||||
}
|
||||
return manuallyFilledFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new event.
|
||||
*
|
||||
@@ -226,19 +416,65 @@ public final class FillEventHistory implements Parcelable {
|
||||
* @param datasetId The dataset the event was on, or {@code null} if the event was on the
|
||||
* whole response.
|
||||
* @param clientState The client state associated with the event.
|
||||
* @param selectedDatasetIds The ids of datasets selected by the user.
|
||||
* @param ignoredDatasetIds The ids of datasets NOT select by the user.
|
||||
* @param changedFieldIds The ids of fields changed by the user.
|
||||
* @param changedDatasetIds The ids of the datasets that havd values matching the
|
||||
* respective entry on {@code changedFieldIds}.
|
||||
* @param manuallyFilledFieldIds The ids of fields that were manually entered by the user
|
||||
* and belonged to datasets.
|
||||
* @param manuallyFilledDatasetIds The ids of datasets that had values matching the
|
||||
* respective entry on {@code manuallyFilledFieldIds}.
|
||||
*
|
||||
* @throws IllegalArgumentException If the length of {@code changedFieldIds} and
|
||||
* {@code changedDatasetIds} doesn't match.
|
||||
* @throws IllegalArgumentException If the length of {@code manuallyFilledFieldIds} and
|
||||
* {@code manuallyFilledDatasetIds} doesn't match.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public Event(int eventType, @Nullable String datasetId, @Nullable Bundle clientState) {
|
||||
mEventType = Preconditions.checkArgumentInRange(eventType, 0, TYPE_SAVE_SHOWN,
|
||||
public Event(int eventType, @Nullable String datasetId, @Nullable Bundle clientState,
|
||||
@Nullable List<String> selectedDatasetIds,
|
||||
@Nullable ArraySet<String> ignoredDatasetIds,
|
||||
@Nullable ArrayList<AutofillId> changedFieldIds,
|
||||
@Nullable ArrayList<String> changedDatasetIds,
|
||||
@Nullable ArrayList<AutofillId> manuallyFilledFieldIds,
|
||||
@Nullable ArrayList<ArrayList<String>> manuallyFilledDatasetIds) {
|
||||
mEventType = Preconditions.checkArgumentInRange(eventType, 0, TYPE_CONTEXT_COMMITTED,
|
||||
"eventType");
|
||||
mDatasetId = datasetId;
|
||||
mClientState = clientState;
|
||||
mSelectedDatasetIds = selectedDatasetIds;
|
||||
mIgnoredDatasetIds = ignoredDatasetIds;
|
||||
if (changedFieldIds != null) {
|
||||
Preconditions.checkArgument(!ArrayUtils.isEmpty(changedFieldIds)
|
||||
&& changedDatasetIds != null
|
||||
&& changedFieldIds.size() == changedDatasetIds.size(),
|
||||
"changed ids must have same length and not be empty");
|
||||
}
|
||||
mChangedFieldIds = changedFieldIds;
|
||||
mChangedDatasetIds = changedDatasetIds;
|
||||
if (manuallyFilledFieldIds != null) {
|
||||
Preconditions.checkArgument(!ArrayUtils.isEmpty(manuallyFilledFieldIds)
|
||||
&& manuallyFilledDatasetIds != null
|
||||
&& manuallyFilledFieldIds.size() == manuallyFilledDatasetIds.size(),
|
||||
"manually filled ids must have same length and not be empty");
|
||||
}
|
||||
mManuallyFilledFieldIds = manuallyFilledFieldIds;
|
||||
mManuallyFilledDatasetIds = manuallyFilledDatasetIds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "FillEvent [datasetId=" + mDatasetId + ", type=" + mEventType + "]";
|
||||
return "FillEvent [datasetId=" + mDatasetId
|
||||
+ ", type=" + mEventType
|
||||
+ ", selectedDatasets=" + mSelectedDatasetIds
|
||||
+ ", ignoredDatasetIds=" + mIgnoredDatasetIds
|
||||
+ ", changedFieldIds=" + mChangedFieldIds
|
||||
+ ", changedDatasetsIds=" + mChangedDatasetIds
|
||||
+ ", manuallyFilledFieldIds=" + mManuallyFilledFieldIds
|
||||
+ ", manuallyFilledDatasetIds=" + mManuallyFilledDatasetIds
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,12 +484,37 @@ public final class FillEventHistory implements Parcelable {
|
||||
public FillEventHistory createFromParcel(Parcel parcel) {
|
||||
FillEventHistory selection = new FillEventHistory(0, 0, parcel.readBundle());
|
||||
|
||||
int numEvents = parcel.readInt();
|
||||
final int numEvents = parcel.readInt();
|
||||
for (int i = 0; i < numEvents; i++) {
|
||||
selection.addEvent(new Event(parcel.readInt(), parcel.readString(),
|
||||
parcel.readBundle()));
|
||||
}
|
||||
final int eventType = parcel.readInt();
|
||||
final String datasetId = parcel.readString();
|
||||
final Bundle clientState = parcel.readBundle();
|
||||
final ArrayList<String> selectedDatasetIds = parcel.createStringArrayList();
|
||||
@SuppressWarnings("unchecked")
|
||||
final ArraySet<String> ignoredDatasets =
|
||||
(ArraySet<String>) parcel.readArraySet(null);
|
||||
final ArrayList<AutofillId> changedFieldIds =
|
||||
parcel.createTypedArrayList(AutofillId.CREATOR);
|
||||
final ArrayList<String> changedDatasetIds = parcel.createStringArrayList();
|
||||
|
||||
final ArrayList<AutofillId> manuallyFilledFieldIds =
|
||||
parcel.createTypedArrayList(AutofillId.CREATOR);
|
||||
final ArrayList<ArrayList<String>> manuallyFilledDatasetIds;
|
||||
if (manuallyFilledFieldIds != null) {
|
||||
final int size = manuallyFilledFieldIds.size();
|
||||
manuallyFilledDatasetIds = new ArrayList<>(size);
|
||||
for (int j = 0; j < size; j++) {
|
||||
manuallyFilledDatasetIds.add(parcel.createStringArrayList());
|
||||
}
|
||||
} else {
|
||||
manuallyFilledDatasetIds = null;
|
||||
}
|
||||
|
||||
selection.addEvent(new Event(eventType, datasetId, clientState,
|
||||
selectedDatasetIds, ignoredDatasets,
|
||||
changedFieldIds, changedDatasetIds,
|
||||
manuallyFilledFieldIds, manuallyFilledDatasetIds));
|
||||
}
|
||||
return selection;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ package android.service.autofill;
|
||||
import static android.service.autofill.FillRequest.INVALID_REQUEST_ID;
|
||||
import static android.view.autofill.Helper.sDebug;
|
||||
|
||||
import android.annotation.IntDef;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.app.Activity;
|
||||
@@ -30,6 +31,8 @@ import android.os.Parcelable;
|
||||
import android.view.autofill.AutofillId;
|
||||
import android.widget.RemoteViews;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -42,6 +45,19 @@ import java.util.List;
|
||||
*/
|
||||
public final class FillResponse implements Parcelable {
|
||||
|
||||
/**
|
||||
* Must be set in the last response to generate
|
||||
* {@link FillEventHistory.Event#TYPE_CONTEXT_COMMITTED} events.
|
||||
*/
|
||||
public static final int FLAG_TRACK_CONTEXT_COMMITED = 0x1;
|
||||
|
||||
/** @hide */
|
||||
@IntDef(flag = true, value = {
|
||||
FLAG_TRACK_CONTEXT_COMMITED
|
||||
})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@interface FillResponseFlags {}
|
||||
|
||||
private final @Nullable ParceledListSlice<Dataset> mDatasets;
|
||||
private final @Nullable SaveInfo mSaveInfo;
|
||||
private final @Nullable Bundle mClientState;
|
||||
@@ -49,6 +65,7 @@ public final class FillResponse implements Parcelable {
|
||||
private final @Nullable IntentSender mAuthentication;
|
||||
private final @Nullable AutofillId[] mAuthenticationIds;
|
||||
private final @Nullable AutofillId[] mIgnoredIds;
|
||||
private final int mFlags;
|
||||
private int mRequestId;
|
||||
|
||||
private FillResponse(@NonNull Builder builder) {
|
||||
@@ -59,6 +76,7 @@ public final class FillResponse implements Parcelable {
|
||||
mAuthentication = builder.mAuthentication;
|
||||
mAuthenticationIds = builder.mAuthenticationIds;
|
||||
mIgnoredIds = builder.mIgnoredIds;
|
||||
mFlags = builder.mFlags;
|
||||
mRequestId = INVALID_REQUEST_ID;
|
||||
}
|
||||
|
||||
@@ -97,6 +115,11 @@ public final class FillResponse implements Parcelable {
|
||||
return mIgnoredIds;
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
public int getFlags() {
|
||||
return mFlags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates a {@link FillResponse} to a request.
|
||||
*
|
||||
@@ -127,6 +150,7 @@ public final class FillResponse implements Parcelable {
|
||||
private IntentSender mAuthentication;
|
||||
private AutofillId[] mAuthenticationIds;
|
||||
private AutofillId[] mIgnoredIds;
|
||||
private int mFlags;
|
||||
private boolean mDestroyed;
|
||||
|
||||
/**
|
||||
@@ -268,6 +292,19 @@ public final class FillResponse implements Parcelable {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets flags changing the response behavior.
|
||||
*
|
||||
* @param flags {@link #FLAG_TRACK_CONTEXT_COMMITED}, or {@code 0}.
|
||||
*
|
||||
* @return This builder.
|
||||
*/
|
||||
public Builder setFlags(@FillResponseFlags int flags) {
|
||||
throwIfDestroyed();
|
||||
mFlags = flags;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new {@link FillResponse} instance.
|
||||
*
|
||||
@@ -311,6 +348,7 @@ public final class FillResponse implements Parcelable {
|
||||
.append(", hasAuthentication=").append(mAuthentication != null)
|
||||
.append(", authenticationIds=").append(Arrays.toString(mAuthenticationIds))
|
||||
.append(", ignoredIds=").append(Arrays.toString(mIgnoredIds))
|
||||
.append(", flags=").append(mFlags)
|
||||
.append("]")
|
||||
.toString();
|
||||
}
|
||||
@@ -333,6 +371,7 @@ public final class FillResponse implements Parcelable {
|
||||
parcel.writeParcelable(mAuthentication, flags);
|
||||
parcel.writeParcelable(mPresentation, flags);
|
||||
parcel.writeParcelableArray(mIgnoredIds, flags);
|
||||
parcel.writeInt(mFlags);
|
||||
parcel.writeInt(mRequestId);
|
||||
}
|
||||
|
||||
@@ -363,8 +402,9 @@ public final class FillResponse implements Parcelable {
|
||||
}
|
||||
|
||||
builder.setIgnoredIds(parcel.readParcelableArray(null, AutofillId.class));
|
||||
final FillResponse response = builder.build();
|
||||
builder.setFlags(parcel.readInt());
|
||||
|
||||
final FillResponse response = builder.build();
|
||||
response.setRequestId(parcel.readInt());
|
||||
|
||||
return response;
|
||||
|
||||
@@ -52,6 +52,7 @@ import android.service.autofill.FillEventHistory.Event;
|
||||
import android.service.autofill.FillResponse;
|
||||
import android.service.autofill.IAutoFillService;
|
||||
import android.text.TextUtils;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.ArraySet;
|
||||
import android.util.DebugUtils;
|
||||
import android.util.LocalLog;
|
||||
@@ -338,6 +339,8 @@ final class AutofillManagerServiceImpl {
|
||||
return;
|
||||
}
|
||||
|
||||
session.logContextCommittedLocked();
|
||||
|
||||
final boolean finished = session.showSaveLocked();
|
||||
if (sVerbose) Slog.v(TAG, "finishSessionLocked(): session finished on save? " + finished);
|
||||
|
||||
@@ -563,8 +566,9 @@ final class AutofillManagerServiceImpl {
|
||||
void setAuthenticationSelected(int sessionId, @Nullable Bundle clientState) {
|
||||
synchronized (mLock) {
|
||||
if (isValidEventLocked("setAuthenticationSelected()", sessionId)) {
|
||||
mEventHistory
|
||||
.addEvent(new Event(Event.TYPE_AUTHENTICATION_SELECTED, null, clientState));
|
||||
mEventHistory.addEvent(
|
||||
new Event(Event.TYPE_AUTHENTICATION_SELECTED, null, clientState, null, null,
|
||||
null, null, null, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -578,7 +582,7 @@ final class AutofillManagerServiceImpl {
|
||||
if (isValidEventLocked("logDatasetAuthenticationSelected()", sessionId)) {
|
||||
mEventHistory.addEvent(
|
||||
new Event(Event.TYPE_DATASET_AUTHENTICATION_SELECTED, selectedDataset,
|
||||
clientState));
|
||||
clientState, null, null, null, null, null, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -589,7 +593,8 @@ final class AutofillManagerServiceImpl {
|
||||
void logSaveShown(int sessionId, @Nullable Bundle clientState) {
|
||||
synchronized (mLock) {
|
||||
if (isValidEventLocked("logSaveShown()", sessionId)) {
|
||||
mEventHistory.addEvent(new Event(Event.TYPE_SAVE_SHOWN, null, clientState));
|
||||
mEventHistory.addEvent(new Event(Event.TYPE_SAVE_SHOWN, null, clientState, null,
|
||||
null, null, null, null, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -602,7 +607,28 @@ final class AutofillManagerServiceImpl {
|
||||
synchronized (mLock) {
|
||||
if (isValidEventLocked("logDatasetSelected()", sessionId)) {
|
||||
mEventHistory.addEvent(
|
||||
new Event(Event.TYPE_DATASET_SELECTED, selectedDataset, clientState));
|
||||
new Event(Event.TYPE_DATASET_SELECTED, selectedDataset, clientState, null,
|
||||
null, null, null, null, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the last fill response when an autofill context is committed.
|
||||
*/
|
||||
void logContextCommitted(int sessionId, @Nullable Bundle clientState,
|
||||
@Nullable ArrayList<String> selectedDatasets,
|
||||
@Nullable ArraySet<String> ignoredDatasets,
|
||||
@Nullable ArrayList<AutofillId> changedFieldIds,
|
||||
@Nullable ArrayList<String> changedDatasetIds,
|
||||
@Nullable ArrayList<AutofillId> manuallyFilledFieldIds,
|
||||
@Nullable ArrayList<ArrayList<String>> manuallyFilledDatasetIds) {
|
||||
synchronized (mLock) {
|
||||
if (isValidEventLocked("logDatasetNotSelected()", sessionId)) {
|
||||
mEventHistory.addEvent(new Event(Event.TYPE_CONTEXT_COMMITTED, null,
|
||||
clientState, selectedDatasets, ignoredDatasets,
|
||||
changedFieldIds, changedDatasetIds,
|
||||
manuallyFilledFieldIds, manuallyFilledDatasetIds));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ import android.os.SystemClock;
|
||||
import android.service.autofill.AutofillService;
|
||||
import android.service.autofill.Dataset;
|
||||
import android.service.autofill.FillContext;
|
||||
import android.service.autofill.FillEventHistory;
|
||||
import android.service.autofill.FillRequest;
|
||||
import android.service.autofill.FillResponse;
|
||||
import android.service.autofill.InternalSanitizer;
|
||||
@@ -90,6 +91,7 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
@@ -839,6 +841,194 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
|
||||
return response == null ? null : response.getSaveInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a {@link android.service.autofill.FillEventHistory.Event#TYPE_CONTEXT_COMMITTED}
|
||||
* when necessary.
|
||||
*/
|
||||
public void logContextCommittedLocked() {
|
||||
if (mResponses == null) {
|
||||
if (sVerbose) Slog.v(TAG, "logContextCommittedLocked(): skipped (no responses)");
|
||||
return;
|
||||
}
|
||||
|
||||
final FillResponse lastResponse = mResponses.valueAt(mResponses.size() -1);
|
||||
final int flags = lastResponse.getFlags();
|
||||
if ((flags & FillResponse.FLAG_TRACK_CONTEXT_COMMITED) == 0) {
|
||||
if (sDebug) Slog.d(TAG, "logContextCommittedLocked(): ignored by flags " + flags);
|
||||
return;
|
||||
}
|
||||
|
||||
ArraySet<String> ignoredDatasets = null;
|
||||
ArrayList<AutofillId> changedFieldIds = null;
|
||||
ArrayList<String> changedDatasetIds = null;
|
||||
ArrayMap<AutofillId, ArraySet<String>> manuallyFilledIds = null;
|
||||
|
||||
boolean hasAtLeastOneDataset = false;
|
||||
final int responseCount = mResponses.size();
|
||||
for (int i = 0; i < responseCount; i++) {
|
||||
final FillResponse response = mResponses.valueAt(i);
|
||||
final List<Dataset> datasets = response.getDatasets();
|
||||
if (datasets == null || datasets.isEmpty()) {
|
||||
if (sVerbose) Slog.v(TAG, "logContextCommitted() no datasets at " + i);
|
||||
} else {
|
||||
for (int j = 0; j < datasets.size(); j++) {
|
||||
final Dataset dataset = datasets.get(j);
|
||||
final String datasetId = dataset.getId();
|
||||
if (datasetId == null) {
|
||||
if (sVerbose) {
|
||||
Slog.v(TAG, "logContextCommitted() skipping idless dataset " + dataset);
|
||||
}
|
||||
} else {
|
||||
hasAtLeastOneDataset = true;
|
||||
if (mSelectedDatasetIds == null
|
||||
|| !mSelectedDatasetIds.contains(datasetId)) {
|
||||
if (sVerbose) Slog.v(TAG, "adding ignored dataset " + datasetId);
|
||||
if (ignoredDatasets == null) {
|
||||
ignoredDatasets = new ArraySet<>();
|
||||
}
|
||||
ignoredDatasets.add(datasetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasAtLeastOneDataset) {
|
||||
if (sVerbose) Slog.v(TAG, "logContextCommittedLocked(): skipped (no datasets)");
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < mViewStates.size(); i++) {
|
||||
final ViewState viewState = mViewStates.valueAt(i);
|
||||
final int state = viewState.getState();
|
||||
|
||||
// When value changed, we need to log if it was:
|
||||
// - autofilled -> changedDatasetIds
|
||||
// - not autofilled but matches a dataset value -> manuallyFilledIds
|
||||
if ((state & ViewState.STATE_CHANGED) != 0) {
|
||||
|
||||
// Check if autofilled value was changed
|
||||
if ((state & ViewState.STATE_AUTOFILLED) != 0) {
|
||||
final String datasetId = viewState.getDatasetId();
|
||||
if (datasetId == null) {
|
||||
// Sanity check - should never happen.
|
||||
Slog.w(TAG, "logContextCommitted(): no dataset id on " + viewState);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Must first check if final changed value is not the same as value sent by
|
||||
// service.
|
||||
final AutofillValue autofilledValue = viewState.getAutofilledValue();
|
||||
final AutofillValue currentValue = viewState.getCurrentValue();
|
||||
if (autofilledValue != null && autofilledValue.equals(currentValue)) {
|
||||
if (sDebug) {
|
||||
Slog.d(TAG, "logContextCommitted(): ignoring changed " + viewState
|
||||
+ " because it has same value that was autofilled");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sDebug) {
|
||||
Slog.d(TAG, "logContextCommitted() found changed state: " + viewState);
|
||||
}
|
||||
if (changedFieldIds == null) {
|
||||
changedFieldIds = new ArrayList<>();
|
||||
changedDatasetIds = new ArrayList<>();
|
||||
}
|
||||
changedFieldIds.add(viewState.id);
|
||||
changedDatasetIds.add(datasetId);
|
||||
} else {
|
||||
// Check if value match a dataset.
|
||||
final AutofillValue currentValue = viewState.getCurrentValue();
|
||||
if (currentValue == null) {
|
||||
if (sDebug) {
|
||||
Slog.d(TAG, "logContextCommitted(): skipping view witout current value "
|
||||
+ "( " + viewState + ")");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (int j = 0; j < responseCount; j++) {
|
||||
final FillResponse response = mResponses.valueAt(j);
|
||||
final List<Dataset> datasets = response.getDatasets();
|
||||
if (datasets == null || datasets.isEmpty()) {
|
||||
if (sVerbose) Slog.v(TAG, "logContextCommitted() no datasets at " + j);
|
||||
} else {
|
||||
for (int k = 0; k < datasets.size(); k++) {
|
||||
final Dataset dataset = datasets.get(k);
|
||||
final String datasetId = dataset.getId();
|
||||
if (datasetId == null) {
|
||||
if (sVerbose) {
|
||||
Slog.v(TAG, "logContextCommitted() skipping idless dataset "
|
||||
+ dataset);
|
||||
}
|
||||
} else {
|
||||
final ArrayList<AutofillValue> values = dataset.getFieldValues();
|
||||
for (int l = 0; l < values.size(); l++) {
|
||||
final AutofillValue candidate = values.get(l);
|
||||
if (currentValue.equals(candidate)) {
|
||||
if (sDebug) {
|
||||
Slog.d(TAG, "field " + viewState.id
|
||||
+ " was manually filled with value set by "
|
||||
+ "dataset " + datasetId);
|
||||
}
|
||||
if (manuallyFilledIds == null) {
|
||||
manuallyFilledIds = new ArrayMap<>();
|
||||
}
|
||||
ArraySet<String> datasetIds =
|
||||
manuallyFilledIds.get(viewState.id);
|
||||
if (datasetIds == null) {
|
||||
datasetIds = new ArraySet<>(1);
|
||||
manuallyFilledIds.put(viewState.id, datasetIds);
|
||||
}
|
||||
datasetIds.add(datasetId);
|
||||
}
|
||||
}
|
||||
if (mSelectedDatasetIds == null
|
||||
|| !mSelectedDatasetIds.contains(datasetId)) {
|
||||
if (sVerbose) {
|
||||
Slog.v(TAG, "adding ignored dataset " + datasetId);
|
||||
}
|
||||
if (ignoredDatasets == null) {
|
||||
ignoredDatasets = new ArraySet<>();
|
||||
}
|
||||
ignoredDatasets.add(datasetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sVerbose) {
|
||||
Slog.v(TAG, "logContextCommitted(): id=" + id
|
||||
+ ", selectedDatasetids=" + mSelectedDatasetIds
|
||||
+ ", ignoredDatasetIds=" + ignoredDatasets
|
||||
+ ", changedAutofillIds=" + changedFieldIds
|
||||
+ ", changedDatasetIds=" + changedDatasetIds
|
||||
+ ", manuallyFilledIds=" + manuallyFilledIds);
|
||||
}
|
||||
|
||||
ArrayList<AutofillId> manuallyFilledFieldIds = null;
|
||||
ArrayList<ArrayList<String>> manuallyFilledDatasetIds = null;
|
||||
|
||||
// Must "flatten" the map to the parcellable collection primitives
|
||||
if (manuallyFilledIds != null) {
|
||||
final int size = manuallyFilledIds.size();
|
||||
manuallyFilledFieldIds = new ArrayList<>(size);
|
||||
manuallyFilledDatasetIds = new ArrayList<>(size);
|
||||
for (int i = 0; i < size; i++) {
|
||||
final AutofillId fieldId = manuallyFilledIds.keyAt(i);
|
||||
final ArraySet<String> datasetIds = manuallyFilledIds.valueAt(i);
|
||||
manuallyFilledFieldIds.add(fieldId);
|
||||
manuallyFilledDatasetIds.add(new ArrayList<>(datasetIds));
|
||||
}
|
||||
}
|
||||
mService.logContextCommitted(id, mClientState, mSelectedDatasetIds, ignoredDatasets,
|
||||
changedFieldIds, changedDatasetIds,
|
||||
manuallyFilledFieldIds, manuallyFilledDatasetIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the save UI, when session can be saved.
|
||||
*
|
||||
@@ -1601,7 +1791,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
|
||||
}
|
||||
|
||||
if (mResponses == null) {
|
||||
mResponses = new SparseArray<>(4);
|
||||
// Set initial capacity as 2 to handle cases where service always requires auth.
|
||||
// TODO: add a metric for number of responses set by server, so we can use its average
|
||||
// as the initial array capacitiy.
|
||||
mResponses = new SparseArray<>(2);
|
||||
}
|
||||
mResponses.put(requestId, newResponse);
|
||||
mClientState = newClientState != null ? newClientState : newResponse.getClientState();
|
||||
@@ -1677,6 +1870,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
|
||||
final AutofillId id = ids.get(j);
|
||||
final AutofillValue value = values.get(j);
|
||||
final ViewState viewState = createOrUpdateViewStateLocked(id, state, value);
|
||||
final String datasetId = dataset.getId();
|
||||
if (datasetId != null) {
|
||||
viewState.setDatasetId(datasetId);
|
||||
}
|
||||
if (response != null) {
|
||||
viewState.setResponse(response);
|
||||
} else if (clearResponse) {
|
||||
|
||||
@@ -78,6 +78,7 @@ final class ViewState {
|
||||
private AutofillValue mAutofilledValue;
|
||||
private Rect mVirtualBounds;
|
||||
private int mState;
|
||||
private String mDatasetId;
|
||||
|
||||
ViewState(Session session, AutofillId id, Listener listener, int state) {
|
||||
mSession = session;
|
||||
@@ -148,6 +149,15 @@ final class ViewState {
|
||||
mState &= ~state;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
String getDatasetId() {
|
||||
return mDatasetId;
|
||||
}
|
||||
|
||||
void setDatasetId(String datasetId) {
|
||||
mDatasetId = datasetId;
|
||||
}
|
||||
|
||||
// TODO: refactor / rename / document this method (and maybeCallOnFillReady) to make it clear
|
||||
// that it can change the value and update the UI; similarly, should replace code that
|
||||
// directly sets mAutofillValue to use encapsulation.
|
||||
@@ -182,13 +192,15 @@ final class ViewState {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ViewState: [id=" + id + ", currentValue=" + mCurrentValue
|
||||
return "ViewState: [id=" + id + ", datasetId=" + mDatasetId
|
||||
+ ", currentValue=" + mCurrentValue
|
||||
+ ", autofilledValue=" + mAutofilledValue
|
||||
+ ", bounds=" + mVirtualBounds + ", state=" + getStateAsString() + "]";
|
||||
}
|
||||
|
||||
void dump(String prefix, PrintWriter pw) {
|
||||
pw.print(prefix); pw.print("id:" ); pw.println(this.id);
|
||||
pw.print(prefix); pw.print("datasetId:" ); pw.println(this.mDatasetId);
|
||||
pw.print(prefix); pw.print("state:" ); pw.println(getStateAsString());
|
||||
pw.print(prefix); pw.print("response:");
|
||||
if (mResponse == null) {
|
||||
|
||||
Reference in New Issue
Block a user