Merge "Separate a11y and autofill view ids" into oc-dev
am: c91290a6e9
Change-Id: Ib4566c2f0e32477d5f9eb6510edc140c2b66e0a4
This commit is contained in:
@@ -719,7 +719,7 @@ public class Activity extends ContextThemeWrapper
|
||||
public static final int FINISH_TASK_WITH_ACTIVITY = 2;
|
||||
|
||||
static final String FRAGMENTS_TAG = "android:fragments";
|
||||
private static final String LAST_ACCESSIBILITY_ID = "android:lastAccessibilityId";
|
||||
private static final String LAST_AUTOFILL_ID = "android:lastAutofillId";
|
||||
|
||||
private static final String AUTOFILL_RESET_NEEDED = "@android:autofillResetNeeded";
|
||||
private static final String WINDOW_HIERARCHY_TAG = "android:viewHierarchyState";
|
||||
@@ -853,8 +853,8 @@ public class Activity extends ContextThemeWrapper
|
||||
|
||||
private boolean mAutoFillResetNeeded;
|
||||
|
||||
/** The last accessibility id that was returned from {@link #getNextAccessibilityId()} */
|
||||
private int mLastAccessibilityId = View.LAST_APP_ACCESSIBILITY_ID;
|
||||
/** The last autofill id that was returned from {@link #getNextAutofillId()} */
|
||||
private int mLastAutofillId = View.LAST_APP_AUTOFILL_ID;
|
||||
|
||||
private AutofillPopupWindow mAutofillPopupWindow;
|
||||
|
||||
@@ -999,7 +999,7 @@ public class Activity extends ContextThemeWrapper
|
||||
}
|
||||
if (savedInstanceState != null) {
|
||||
mAutoFillResetNeeded = savedInstanceState.getBoolean(AUTOFILL_RESET_NEEDED, false);
|
||||
mLastAccessibilityId = savedInstanceState.getInt(LAST_ACCESSIBILITY_ID, View.NO_ID);
|
||||
mLastAutofillId = savedInstanceState.getInt(LAST_AUTOFILL_ID, View.NO_ID);
|
||||
|
||||
if (mAutoFillResetNeeded) {
|
||||
getAutofillManager().onCreate(savedInstanceState);
|
||||
@@ -1348,24 +1348,23 @@ public class Activity extends ContextThemeWrapper
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next accessibility ID.
|
||||
* Gets the next autofill ID.
|
||||
*
|
||||
* <p>All IDs will be bigger than {@link View#LAST_APP_ACCESSIBILITY_ID}. All IDs returned
|
||||
* <p>All IDs will be bigger than {@link View#LAST_APP_AUTOFILL_ID}. All IDs returned
|
||||
* will be unique.
|
||||
*
|
||||
* @return A ID that is unique in the activity
|
||||
*
|
||||
* {@hide}
|
||||
*/
|
||||
@Override
|
||||
public int getNextAccessibilityId() {
|
||||
if (mLastAccessibilityId == Integer.MAX_VALUE - 1) {
|
||||
mLastAccessibilityId = View.LAST_APP_ACCESSIBILITY_ID;
|
||||
public int getNextAutofillId() {
|
||||
if (mLastAutofillId == Integer.MAX_VALUE - 1) {
|
||||
mLastAutofillId = View.LAST_APP_AUTOFILL_ID;
|
||||
}
|
||||
|
||||
mLastAccessibilityId++;
|
||||
mLastAutofillId++;
|
||||
|
||||
return mLastAccessibilityId;
|
||||
return mLastAutofillId;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1563,7 +1562,7 @@ public class Activity extends ContextThemeWrapper
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
|
||||
|
||||
outState.putInt(LAST_ACCESSIBILITY_ID, mLastAccessibilityId);
|
||||
outState.putInt(LAST_AUTOFILL_ID, mLastAutofillId);
|
||||
Parcelable p = mFragments.saveAllState();
|
||||
if (p != null) {
|
||||
outState.putParcelable(FRAGMENTS_TAG, p);
|
||||
@@ -7455,7 +7454,7 @@ public class Activity extends ContextThemeWrapper
|
||||
|
||||
/** @hide */
|
||||
@Override
|
||||
@NonNull public View[] findViewsByAccessibilityIdTraversal(@NonNull int[] viewIds) {
|
||||
@NonNull public View[] findViewsByAutofillIdTraversal(@NonNull int[] viewIds) {
|
||||
final View[] views = new View[viewIds.length];
|
||||
final ArrayList<ViewRootImpl> roots =
|
||||
WindowManagerGlobal.getInstance().getRootViews(getActivityToken());
|
||||
@@ -7466,7 +7465,7 @@ public class Activity extends ContextThemeWrapper
|
||||
if (rootView != null) {
|
||||
for (int viewNum = 0; viewNum < viewIds.length; viewNum++) {
|
||||
if (views[viewNum] == null) {
|
||||
views[viewNum] = rootView.findViewByAccessibilityIdTraversal(
|
||||
views[viewNum] = rootView.findViewByAutofillIdTraversal(
|
||||
viewIds[viewNum]);
|
||||
}
|
||||
}
|
||||
@@ -7478,14 +7477,14 @@ public class Activity extends ContextThemeWrapper
|
||||
|
||||
/** @hide */
|
||||
@Override
|
||||
@Nullable public View findViewByAccessibilityIdTraversal(int viewId) {
|
||||
@Nullable public View findViewByAutofillIdTraversal(int viewId) {
|
||||
final ArrayList<ViewRootImpl> roots =
|
||||
WindowManagerGlobal.getInstance().getRootViews(getActivityToken());
|
||||
for (int rootNum = 0; rootNum < roots.size(); rootNum++) {
|
||||
final View rootView = roots.get(rootNum).getView();
|
||||
|
||||
if (rootView != null) {
|
||||
final View view = rootView.findViewByAccessibilityIdTraversal(viewId);
|
||||
final View view = rootView.findViewByAutofillIdTraversal(viewId);
|
||||
if (view != null) {
|
||||
return view;
|
||||
}
|
||||
@@ -7499,7 +7498,7 @@ public class Activity extends ContextThemeWrapper
|
||||
@Override
|
||||
@NonNull public boolean[] getViewVisibility(@NonNull int[] viewIds) {
|
||||
final boolean[] isVisible = new boolean[viewIds.length];
|
||||
final View views[] = findViewsByAccessibilityIdTraversal(viewIds);
|
||||
final View views[] = findViewsByAutofillIdTraversal(viewIds);
|
||||
|
||||
for (int i = 0; i < viewIds.length; i++) {
|
||||
View view = views[i];
|
||||
|
||||
@@ -488,27 +488,27 @@ public abstract class Context {
|
||||
*/
|
||||
public abstract Context getApplicationContext();
|
||||
|
||||
/** Non-activity related accessibility ids are unique in the app */
|
||||
private static int sLastAccessibilityId = View.NO_ID;
|
||||
/** Non-activity related autofill ids are unique in the app */
|
||||
private static int sLastAutofillId = View.NO_ID;
|
||||
|
||||
/**
|
||||
* Gets the next accessibility ID.
|
||||
* Gets the next autofill ID.
|
||||
*
|
||||
* <p>All IDs will be smaller or the same as {@link View#LAST_APP_ACCESSIBILITY_ID}. All IDs
|
||||
* <p>All IDs will be smaller or the same as {@link View#LAST_APP_AUTOFILL_ID}. All IDs
|
||||
* returned will be unique.
|
||||
*
|
||||
* @return A ID that is unique in the process
|
||||
*
|
||||
* {@hide}
|
||||
*/
|
||||
public int getNextAccessibilityId() {
|
||||
if (sLastAccessibilityId == View.LAST_APP_ACCESSIBILITY_ID - 1) {
|
||||
sLastAccessibilityId = View.NO_ID;
|
||||
public int getNextAutofillId() {
|
||||
if (sLastAutofillId == View.LAST_APP_AUTOFILL_ID - 1) {
|
||||
sLastAutofillId = View.NO_ID;
|
||||
}
|
||||
|
||||
sLastAccessibilityId++;
|
||||
sLastAutofillId++;
|
||||
|
||||
return sLastAccessibilityId;
|
||||
return sLastAutofillId;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -956,8 +956,7 @@ public class ContextWrapper extends Context {
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@Override
|
||||
public int getNextAccessibilityId() {
|
||||
return mBase.getNextAccessibilityId();
|
||||
public int getNextAutofillId() {
|
||||
return mBase.getNextAutofillId();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -802,7 +802,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|
||||
*
|
||||
* {@hide}
|
||||
*/
|
||||
public static final int LAST_APP_ACCESSIBILITY_ID = Integer.MAX_VALUE / 2;
|
||||
public static final int LAST_APP_AUTOFILL_ID = Integer.MAX_VALUE / 2;
|
||||
|
||||
/**
|
||||
* Attribute to find the autofilled highlight
|
||||
@@ -2044,6 +2044,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|
||||
*/
|
||||
private SparseArray<Object> mKeyedTags;
|
||||
|
||||
/**
|
||||
* The next available accessibility id.
|
||||
*/
|
||||
private static int sNextAccessibilityViewId;
|
||||
|
||||
/**
|
||||
* The animation currently associated with this view.
|
||||
* @hide
|
||||
@@ -2086,16 +2091,19 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|
||||
@ViewDebug.ExportedProperty(resolveId = true)
|
||||
int mID = NO_ID;
|
||||
|
||||
/** The ID of this view for accessibility and autofill purposes.
|
||||
/** The ID of this view for autofill purposes.
|
||||
* <ul>
|
||||
* <li>== {@link #NO_ID}: ID has not been assigned yet
|
||||
* <li>≤ {@link #LAST_APP_ACCESSIBILITY_ID}: View is not part of a activity. The ID is
|
||||
* <li>≤ {@link #LAST_APP_AUTOFILL_ID}: View is not part of a activity. The ID is
|
||||
* unique in the process. This might change
|
||||
* over activity lifecycle events.
|
||||
* <li>> {@link #LAST_APP_ACCESSIBILITY_ID}: View is part of a activity. The ID is
|
||||
* <li>> {@link #LAST_APP_AUTOFILL_ID}: View is part of a activity. The ID is
|
||||
* unique in the activity. This stays the same
|
||||
* over activity lifecycle events.
|
||||
*/
|
||||
private int mAutofillViewId = NO_ID;
|
||||
|
||||
// ID for accessibility purposes. This ID must be unique for every window
|
||||
private int mAccessibilityViewId = NO_ID;
|
||||
|
||||
private int mAccessibilityCursorPosition = ACCESSIBILITY_CURSOR_POSITION_UNDEFINED;
|
||||
@@ -7723,7 +7731,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|
||||
if (mAutofillId == null) {
|
||||
// The autofill id needs to be unique, but its value doesn't matter,
|
||||
// so it's better to reuse the accessibility id to save space.
|
||||
mAutofillId = new AutofillId(getAccessibilityViewId());
|
||||
mAutofillId = new AutofillId(getAutofillViewId());
|
||||
}
|
||||
return mAutofillId;
|
||||
}
|
||||
@@ -7956,7 +7964,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|
||||
|
||||
private boolean isAutofillable() {
|
||||
return getAutofillType() != AUTOFILL_TYPE_NONE && isImportantForAutofill()
|
||||
&& getAccessibilityViewId() > LAST_APP_ACCESSIBILITY_ID;
|
||||
&& getAutofillViewId() > LAST_APP_AUTOFILL_ID;
|
||||
}
|
||||
|
||||
private void populateVirtualStructure(ViewStructure structure,
|
||||
@@ -8474,11 +8482,25 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|
||||
*/
|
||||
public int getAccessibilityViewId() {
|
||||
if (mAccessibilityViewId == NO_ID) {
|
||||
mAccessibilityViewId = mContext.getNextAccessibilityId();
|
||||
mAccessibilityViewId = sNextAccessibilityViewId++;
|
||||
}
|
||||
return mAccessibilityViewId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the unique identifier of this view on the screen for autofill purposes.
|
||||
*
|
||||
* @return The view autofill id.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public int getAutofillViewId() {
|
||||
if (mAutofillViewId == NO_ID) {
|
||||
mAutofillViewId = mContext.getNextAutofillId();
|
||||
}
|
||||
return mAutofillViewId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the unique identifier of the window in which this View reseides.
|
||||
*
|
||||
@@ -12117,7 +12139,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|
||||
if (isAutofillable()) {
|
||||
AutofillManager afm = getAutofillManager();
|
||||
|
||||
if (afm != null && getAccessibilityViewId() > LAST_APP_ACCESSIBILITY_ID) {
|
||||
if (afm != null && getAutofillViewId() > LAST_APP_AUTOFILL_ID) {
|
||||
if (mVisibilityChangeForAutofillHandler != null) {
|
||||
mVisibilityChangeForAutofillHandler.removeMessages(0);
|
||||
}
|
||||
@@ -17547,16 +17569,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|
||||
*
|
||||
* @return Returns a Parcelable object containing the view's current dynamic
|
||||
* state, or null if there is nothing interesting to save.
|
||||
* @see #onRestoreInstanceState(android.os.Parcelable)
|
||||
* @see #saveHierarchyState(android.util.SparseArray)
|
||||
* @see #dispatchSaveInstanceState(android.util.SparseArray)
|
||||
* @see #onRestoreInstanceState(Parcelable)
|
||||
* @see #saveHierarchyState(SparseArray)
|
||||
* @see #dispatchSaveInstanceState(SparseArray)
|
||||
* @see #setSaveEnabled(boolean)
|
||||
*/
|
||||
@CallSuper
|
||||
@Nullable protected Parcelable onSaveInstanceState() {
|
||||
mPrivateFlags |= PFLAG_SAVE_STATE_CALLED;
|
||||
if (mStartActivityRequestWho != null || isAutofilled()
|
||||
|| mAccessibilityViewId > LAST_APP_ACCESSIBILITY_ID) {
|
||||
|| mAutofillViewId > LAST_APP_AUTOFILL_ID) {
|
||||
BaseSavedState state = new BaseSavedState(AbsSavedState.EMPTY_STATE);
|
||||
|
||||
if (mStartActivityRequestWho != null) {
|
||||
@@ -17567,13 +17589,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|
||||
state.mSavedData |= BaseSavedState.IS_AUTOFILLED;
|
||||
}
|
||||
|
||||
if (mAccessibilityViewId > LAST_APP_ACCESSIBILITY_ID) {
|
||||
state.mSavedData |= BaseSavedState.ACCESSIBILITY_ID;
|
||||
if (mAutofillViewId > LAST_APP_AUTOFILL_ID) {
|
||||
state.mSavedData |= BaseSavedState.AUTOFILL_ID;
|
||||
}
|
||||
|
||||
state.mStartActivityRequestWhoSaved = mStartActivityRequestWho;
|
||||
state.mIsAutofilled = isAutofilled();
|
||||
state.mAccessibilityViewId = mAccessibilityViewId;
|
||||
state.mAutofillViewId = mAutofillViewId;
|
||||
return state;
|
||||
}
|
||||
return BaseSavedState.EMPTY_STATE;
|
||||
@@ -17651,8 +17673,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|
||||
if ((baseState.mSavedData & BaseSavedState.IS_AUTOFILLED) != 0) {
|
||||
setAutofilled(baseState.mIsAutofilled);
|
||||
}
|
||||
if ((baseState.mSavedData & BaseSavedState.ACCESSIBILITY_ID) != 0) {
|
||||
mAccessibilityViewId = baseState.mAccessibilityViewId;
|
||||
if ((baseState.mSavedData & BaseSavedState.AUTOFILL_ID) != 0) {
|
||||
mAutofillViewId = baseState.mAutofillViewId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21476,7 +21498,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|
||||
* @param accessibilityId The searched accessibility id.
|
||||
* @return The found view.
|
||||
*/
|
||||
final <T extends View> T findViewByAccessibilityId(int accessibilityId) {
|
||||
final <T extends View> T findViewByAccessibilityId(int accessibilityId) {
|
||||
if (accessibilityId < 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -21488,11 +21510,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the traversal to find a view by its unuque and stable accessibility id.
|
||||
* Performs the traversal to find a view by its unique and stable accessibility id.
|
||||
*
|
||||
* <strong>Note:</strong>This method does not stop at the root namespace
|
||||
* boundary since the user can touch the screen at an arbitrary location
|
||||
* potentially crossing the root namespace bounday which will send an
|
||||
* potentially crossing the root namespace boundary which will send an
|
||||
* accessibility event to accessibility services and they should be able
|
||||
* to obtain the event source. Also accessibility ids are guaranteed to be
|
||||
* unique in the window.
|
||||
@@ -21508,6 +21530,23 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the traversal to find a view by its autofill id.
|
||||
*
|
||||
* <strong>Note:</strong>This method does not stop at the root namespace
|
||||
* boundary.
|
||||
*
|
||||
* @param autofillId The autofill id.
|
||||
* @return The found view.
|
||||
* @hide
|
||||
*/
|
||||
public <T extends View> T findViewByAutofillIdTraversal(int autofillId) {
|
||||
if (getAutofillViewId() == autofillId) {
|
||||
return (T) this;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for a child view with the given tag. If this view has the given
|
||||
* tag, return this view.
|
||||
@@ -24974,13 +25013,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|
||||
public static class BaseSavedState extends AbsSavedState {
|
||||
static final int START_ACTIVITY_REQUESTED_WHO_SAVED = 0b1;
|
||||
static final int IS_AUTOFILLED = 0b10;
|
||||
static final int ACCESSIBILITY_ID = 0b100;
|
||||
static final int AUTOFILL_ID = 0b100;
|
||||
|
||||
// Flags that describe what data in this state is valid
|
||||
int mSavedData;
|
||||
String mStartActivityRequestWhoSaved;
|
||||
boolean mIsAutofilled;
|
||||
int mAccessibilityViewId;
|
||||
int mAutofillViewId;
|
||||
|
||||
/**
|
||||
* Constructor used when reading from a parcel. Reads the state of the superclass.
|
||||
@@ -25003,7 +25042,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|
||||
mSavedData = source.readInt();
|
||||
mStartActivityRequestWhoSaved = source.readString();
|
||||
mIsAutofilled = source.readBoolean();
|
||||
mAccessibilityViewId = source.readInt();
|
||||
mAutofillViewId = source.readInt();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25022,7 +25061,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|
||||
out.writeInt(mSavedData);
|
||||
out.writeString(mStartActivityRequestWhoSaved);
|
||||
out.writeBoolean(mIsAutofilled);
|
||||
out.writeInt(mAccessibilityViewId);
|
||||
out.writeInt(mAutofillViewId);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<BaseSavedState> CREATOR
|
||||
|
||||
@@ -1361,6 +1361,27 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@Override
|
||||
public View findViewByAutofillIdTraversal(int autofillId) {
|
||||
View foundView = super.findViewByAutofillIdTraversal(autofillId);
|
||||
if (foundView != null) {
|
||||
return foundView;
|
||||
}
|
||||
|
||||
final int childrenCount = mChildrenCount;
|
||||
final View[] children = mChildren;
|
||||
for (int i = 0; i < childrenCount; i++) {
|
||||
View child = children[i];
|
||||
foundView = child.findViewByAutofillIdTraversal(autofillId);
|
||||
if (foundView != null) {
|
||||
return foundView;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispatchWindowFocusChanged(boolean hasFocus) {
|
||||
super.dispatchWindowFocusChanged(hasFocus);
|
||||
|
||||
@@ -246,20 +246,20 @@ public final class AutofillManager {
|
||||
/**
|
||||
* Finds views by traversing the hierarchies of the client.
|
||||
*
|
||||
* @param viewIds The accessibility ids of the views to find
|
||||
* @param viewIds The autofill ids of the views to find
|
||||
*
|
||||
* @return And array containing the views (empty if no views found).
|
||||
*/
|
||||
@NonNull View[] findViewsByAccessibilityIdTraversal(@NonNull int[] viewIds);
|
||||
@NonNull View[] findViewsByAutofillIdTraversal(@NonNull int[] viewIds);
|
||||
|
||||
/**
|
||||
* Finds a view by traversing the hierarchies of the client.
|
||||
*
|
||||
* @param viewId The accessibility id of the views to find
|
||||
* @param viewId The autofill id of the views to find
|
||||
*
|
||||
* @return The view, or {@code null} if not found
|
||||
*/
|
||||
@Nullable View findViewByAccessibilityIdTraversal(int viewId);
|
||||
@Nullable View findViewByAutofillIdTraversal(int viewId);
|
||||
|
||||
/**
|
||||
* Runs the specified action on the UI thread.
|
||||
@@ -795,11 +795,11 @@ public final class AutofillManager {
|
||||
}
|
||||
|
||||
private static AutofillId getAutofillId(View view) {
|
||||
return new AutofillId(view.getAccessibilityViewId());
|
||||
return new AutofillId(view.getAutofillViewId());
|
||||
}
|
||||
|
||||
private static AutofillId getAutofillId(View parent, int virtualId) {
|
||||
return new AutofillId(parent.getAccessibilityViewId(), virtualId);
|
||||
return new AutofillId(parent.getAutofillViewId(), virtualId);
|
||||
}
|
||||
|
||||
private void startSessionLocked(@NonNull AutofillId id, @NonNull Rect bounds,
|
||||
@@ -1039,7 +1039,7 @@ public final class AutofillManager {
|
||||
final int itemCount = ids.size();
|
||||
int numApplied = 0;
|
||||
ArrayMap<View, SparseArray<AutofillValue>> virtualValues = null;
|
||||
final View[] views = client.findViewsByAccessibilityIdTraversal(getViewIds(ids));
|
||||
final View[] views = client.findViewsByAutofillIdTraversal(getViewIds(ids));
|
||||
|
||||
for (int i = 0; i < itemCount; i++) {
|
||||
final AutofillId id = ids.get(i);
|
||||
@@ -1232,7 +1232,7 @@ public final class AutofillManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
return client.findViewByAccessibilityIdTraversal(autofillId.getViewId());
|
||||
return client.findViewByAutofillIdTraversal(autofillId.getViewId());
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
|
||||
Reference in New Issue
Block a user