ImportKeys: Add basic key importing
This commit is contained in:
@@ -268,6 +268,7 @@ public class ImportKeysActivity extends BaseActivity implements ImportKeysListen
|
|||||||
*/
|
*/
|
||||||
private void startListFragment(byte[] bytes, Uri dataUri, String serverQuery,
|
private void startListFragment(byte[] bytes, Uri dataUri, String serverQuery,
|
||||||
Preferences.CloudSearchPrefs cloudSearchPrefs) {
|
Preferences.CloudSearchPrefs cloudSearchPrefs) {
|
||||||
|
|
||||||
Fragment listFragment =
|
Fragment listFragment =
|
||||||
ImportKeysListFragment.newInstance(bytes, dataUri, serverQuery, false,
|
ImportKeysListFragment.newInstance(bytes, dataUri, serverQuery, false,
|
||||||
cloudSearchPrefs);
|
cloudSearchPrefs);
|
||||||
|
|||||||
@@ -32,8 +32,12 @@ import android.support.v4.util.LongSparseArray;
|
|||||||
import android.support.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.sufficientlysecure.keychain.Constants;
|
import org.sufficientlysecure.keychain.Constants;
|
||||||
import org.sufficientlysecure.keychain.R;
|
import org.sufficientlysecure.keychain.R;
|
||||||
@@ -55,6 +59,7 @@ import org.sufficientlysecure.keychain.util.IteratorWithSize;
|
|||||||
import org.sufficientlysecure.keychain.util.Log;
|
import org.sufficientlysecure.keychain.util.Log;
|
||||||
import org.sufficientlysecure.keychain.util.ParcelableProxy;
|
import org.sufficientlysecure.keychain.util.ParcelableProxy;
|
||||||
import org.sufficientlysecure.keychain.util.Preferences;
|
import org.sufficientlysecure.keychain.util.Preferences;
|
||||||
|
import org.sufficientlysecure.keychain.util.Preferences.CloudSearchPrefs;
|
||||||
import org.sufficientlysecure.keychain.util.orbot.OrbotHelper;
|
import org.sufficientlysecure.keychain.util.orbot.OrbotHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -78,6 +83,7 @@ public class ImportKeysListFragment extends Fragment implements
|
|||||||
|
|
||||||
private RecyclerView mRecyclerView;
|
private RecyclerView mRecyclerView;
|
||||||
private ImportKeysAdapter mAdapter;
|
private ImportKeysAdapter mAdapter;
|
||||||
|
private boolean mAdvanced;
|
||||||
|
|
||||||
private LoaderState mLoaderState;
|
private LoaderState mLoaderState;
|
||||||
|
|
||||||
@@ -148,7 +154,8 @@ public class ImportKeysListFragment extends Fragment implements
|
|||||||
* @return fragment with arguments set based on passed parameters
|
* @return fragment with arguments set based on passed parameters
|
||||||
*/
|
*/
|
||||||
public static ImportKeysListFragment newInstance(byte[] bytes, Uri dataUri, String serverQuery,
|
public static ImportKeysListFragment newInstance(byte[] bytes, Uri dataUri, String serverQuery,
|
||||||
Preferences.CloudSearchPrefs cloudSearchPrefs) {
|
CloudSearchPrefs cloudSearchPrefs) {
|
||||||
|
|
||||||
return newInstance(bytes, dataUri, serverQuery, false, cloudSearchPrefs);
|
return newInstance(bytes, dataUri, serverQuery, false, cloudSearchPrefs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,8 +175,7 @@ public class ImportKeysListFragment extends Fragment implements
|
|||||||
Uri dataUri,
|
Uri dataUri,
|
||||||
String serverQuery,
|
String serverQuery,
|
||||||
boolean nonInteractive,
|
boolean nonInteractive,
|
||||||
Preferences.CloudSearchPrefs cloudSearchPrefs) {
|
CloudSearchPrefs cloudSearchPrefs) {
|
||||||
ImportKeysListFragment frag = new ImportKeysListFragment();
|
|
||||||
|
|
||||||
Bundle args = new Bundle();
|
Bundle args = new Bundle();
|
||||||
args.putByteArray(ARG_BYTES, bytes);
|
args.putByteArray(ARG_BYTES, bytes);
|
||||||
@@ -178,13 +184,13 @@ public class ImportKeysListFragment extends Fragment implements
|
|||||||
args.putBoolean(ARG_NON_INTERACTIVE, nonInteractive);
|
args.putBoolean(ARG_NON_INTERACTIVE, nonInteractive);
|
||||||
args.putParcelable(ARG_CLOUD_SEARCH_PREFS, cloudSearchPrefs);
|
args.putParcelable(ARG_CLOUD_SEARCH_PREFS, cloudSearchPrefs);
|
||||||
|
|
||||||
|
ImportKeysListFragment frag = new ImportKeysListFragment();
|
||||||
frag.setArguments(args);
|
frag.setArguments(args);
|
||||||
|
|
||||||
return frag;
|
return frag;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
|
||||||
binding = DataBindingUtil.inflate(inflater, R.layout.import_keys_list_fragment, container, false);
|
binding = DataBindingUtil.inflate(inflater, R.layout.import_keys_list_fragment, container, false);
|
||||||
binding.setStatus(STATUS_FIRST);
|
binding.setStatus(STATUS_FIRST);
|
||||||
View view = binding.getRoot();
|
View view = binding.getRoot();
|
||||||
@@ -208,7 +214,7 @@ public class ImportKeysListFragment extends Fragment implements
|
|||||||
if (dataUri != null || bytes != null) {
|
if (dataUri != null || bytes != null) {
|
||||||
mLoaderState = new BytesLoaderState(bytes, dataUri);
|
mLoaderState = new BytesLoaderState(bytes, dataUri);
|
||||||
} else if (query != null) {
|
} else if (query != null) {
|
||||||
Preferences.CloudSearchPrefs cloudSearchPrefs
|
CloudSearchPrefs cloudSearchPrefs
|
||||||
= args.getParcelable(ARG_CLOUD_SEARCH_PREFS);
|
= args.getParcelable(ARG_CLOUD_SEARCH_PREFS);
|
||||||
if (cloudSearchPrefs == null) {
|
if (cloudSearchPrefs == null) {
|
||||||
cloudSearchPrefs = Preferences.getPreferences(mActivity).getCloudSearchPrefs();
|
cloudSearchPrefs = Preferences.getPreferences(mActivity).getCloudSearchPrefs();
|
||||||
@@ -221,6 +227,16 @@ public class ImportKeysListFragment extends Fragment implements
|
|||||||
restartLoaders();
|
restartLoaders();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setHasOptionsMenu(true);
|
||||||
|
|
||||||
|
TextView importAllKeys = (TextView) view.findViewById(R.id.import_keys);
|
||||||
|
importAllKeys.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
mCallback.importKeys();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,6 +252,38 @@ public class ImportKeysListFragment extends Fragment implements
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||||
|
inflater.inflate(R.menu.import_keys_list_fragment, menu);
|
||||||
|
|
||||||
|
menu.findItem(R.id.basic).setVisible(mAdvanced);
|
||||||
|
menu.findItem(R.id.advanced).setVisible(!mAdvanced);
|
||||||
|
|
||||||
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
|
switch (item.getItemId()) {
|
||||||
|
case R.id.basic:
|
||||||
|
mAdvanced = false;
|
||||||
|
mAdapter.setAdvanced(false);
|
||||||
|
mActivity.invalidateOptionsMenu();
|
||||||
|
|
||||||
|
binding.setAdvanced(mAdvanced);
|
||||||
|
return true;
|
||||||
|
case R.id.advanced:
|
||||||
|
mAdvanced = true;
|
||||||
|
mAdapter.setAdvanced(true);
|
||||||
|
mActivity.invalidateOptionsMenu();
|
||||||
|
|
||||||
|
binding.setAdvanced(mAdvanced);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRequestPermissionsResult(int requestCode,
|
public void onRequestPermissionsResult(int requestCode,
|
||||||
@NonNull String[] permissions,
|
@NonNull String[] permissions,
|
||||||
@@ -263,6 +311,8 @@ public class ImportKeysListFragment extends Fragment implements
|
|||||||
!PermissionsUtil.checkAndRequestReadPermission(mActivity, ls.mDataUri)) {
|
!PermissionsUtil.checkAndRequestReadPermission(mActivity, ls.mDataUri)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} else if (mLoaderState instanceof CloudLoaderState) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
restartLoaders();
|
restartLoaders();
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ public class ImportKeysAdapter extends RecyclerView.Adapter<ImportKeysAdapter.Vi
|
|||||||
|
|
||||||
private FragmentActivity mActivity;
|
private FragmentActivity mActivity;
|
||||||
private ImportKeysResultListener mListener;
|
private ImportKeysResultListener mListener;
|
||||||
private boolean mNonInteractive;
|
private boolean mAdvanced, mNonInteractive;
|
||||||
|
|
||||||
private LoaderState mLoaderState;
|
private LoaderState mLoaderState;
|
||||||
private List<ImportKeysListEntry> mData;
|
private List<ImportKeysListEntry> mData;
|
||||||
@@ -60,14 +60,22 @@ public class ImportKeysAdapter extends RecyclerView.Adapter<ImportKeysAdapter.Vi
|
|||||||
private KeyState[] mKeyStates;
|
private KeyState[] mKeyStates;
|
||||||
private int mCurrent;
|
private int mCurrent;
|
||||||
|
|
||||||
public ImportKeysAdapter(FragmentActivity activity, ImportKeysListener listener, boolean mNonInteractive) {
|
public ImportKeysAdapter(FragmentActivity activity, ImportKeysListener listener,
|
||||||
|
boolean nonInteractive) {
|
||||||
|
|
||||||
this.mActivity = activity;
|
this.mActivity = activity;
|
||||||
this.mListener = listener;
|
this.mListener = listener;
|
||||||
this.mNonInteractive = mNonInteractive;
|
this.mNonInteractive = nonInteractive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAdvanced(boolean advanced) {
|
||||||
|
this.mAdvanced = advanced;
|
||||||
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLoaderState(LoaderState loaderState) {
|
public void setLoaderState(LoaderState loaderState) {
|
||||||
this.mLoaderState = loaderState;
|
this.mLoaderState = loaderState;
|
||||||
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setData(List<ImportKeysListEntry> data) {
|
public void setData(List<ImportKeysListEntry> data) {
|
||||||
@@ -128,6 +136,8 @@ public class ImportKeysAdapter extends RecyclerView.Adapter<ImportKeysAdapter.Vi
|
|||||||
final ImportKeysListEntry entry = mData.get(position);
|
final ImportKeysListEntry entry = mData.get(position);
|
||||||
b.setEntry(entry);
|
b.setEntry(entry);
|
||||||
|
|
||||||
|
b.setAdvanced(mAdvanced);
|
||||||
|
|
||||||
final KeyState keyState = mKeyStates[position];
|
final KeyState keyState = mKeyStates[position];
|
||||||
final boolean downloaded = keyState.mDownloaded;
|
final boolean downloaded = keyState.mDownloaded;
|
||||||
final boolean showed = keyState.mShowed;
|
final boolean showed = keyState.mShowed;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<data>
|
<data>
|
||||||
|
|
||||||
@@ -9,12 +11,11 @@
|
|||||||
alias="i"
|
alias="i"
|
||||||
type="org.sufficientlysecure.keychain.ui.ImportKeysListFragment" />
|
type="org.sufficientlysecure.keychain.ui.ImportKeysListFragment" />
|
||||||
|
|
||||||
<variable
|
<variable name="status" type="int" />
|
||||||
name="status"
|
<variable name="advanced" type="boolean" />
|
||||||
type="int" />
|
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
<FrameLayout
|
<RelativeLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@color/md_grey_100">
|
android:background="@color/md_grey_100">
|
||||||
@@ -22,23 +23,60 @@
|
|||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_centerInParent="true"
|
||||||
android:visibility="@{status == i.STATUS_LOADING ? View.VISIBLE : View.GONE}" />
|
android:visibility="@{status == i.STATUS_LOADING ? View.VISIBLE : View.GONE}" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:text="@string/error_nothing_import"
|
||||||
|
android:visibility="@{status == i.STATUS_EMPTY ? View.VISIBLE : View.GONE}" />
|
||||||
|
|
||||||
<android.support.v7.widget.RecyclerView
|
<android.support.v7.widget.RecyclerView
|
||||||
android:id="@+id/recycler_view"
|
android:id="@+id/recycler_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:layout_above="@+id/toolbar_bottom"
|
||||||
android:paddingLeft="8dp"
|
android:paddingLeft="8dp"
|
||||||
android:paddingRight="8dp"
|
android:paddingRight="8dp"
|
||||||
android:scrollbars="vertical"
|
android:scrollbars="vertical"
|
||||||
android:visibility="@{status == i.STATUS_LOADED ? View.VISIBLE : View.GONE}" />
|
android:visibility="@{status == i.STATUS_LOADED ? View.VISIBLE : View.GONE}" />
|
||||||
|
|
||||||
<TextView
|
<android.support.v7.widget.Toolbar
|
||||||
android:layout_width="wrap_content"
|
android:id="@+id/toolbar_bottom"
|
||||||
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_alignParentBottom="true"
|
||||||
android:text="@string/error_nothing_import"
|
android:background="?attr/colorPrimary"
|
||||||
android:visibility="@{status == i.STATUS_EMPTY ? View.VISIBLE : View.GONE}" />
|
android:elevation="4dp"
|
||||||
</FrameLayout>
|
android:minHeight="?attr/actionBarSize"
|
||||||
|
android:paddingLeft="8dp"
|
||||||
|
android:paddingRight="8dp"
|
||||||
|
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
|
||||||
|
android:visibility="@{!advanced && (status == i.STATUS_LOADED) ? View.VISIBLE : View.GONE}"
|
||||||
|
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
|
||||||
|
tools:ignore="UnusedAttribute">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/import_keys"
|
||||||
|
style="@style/TextAppearance.Widget.AppCompat.Toolbar.Subtitle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:drawablePadding="8dp"
|
||||||
|
android:drawableRight="@drawable/ic_file_download_white_24dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:text="@string/menu_import_all_keys"
|
||||||
|
android:textColor="#ffffffff" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
</android.support.v7.widget.Toolbar>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
</layout>
|
</layout>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<import type="android.view.View" />
|
<import type="android.view.View" />
|
||||||
<import type="org.sufficientlysecure.keychain.keyimport.ImportKeysListEntry" />
|
<import type="org.sufficientlysecure.keychain.keyimport.ImportKeysListEntry" />
|
||||||
|
|
||||||
|
<variable name="advanced" type="boolean" />
|
||||||
<variable name="nonInteractive" type="boolean" />
|
<variable name="nonInteractive" type="boolean" />
|
||||||
<variable name="entry" type="ImportKeysListEntry" />
|
<variable name="entry" type="ImportKeysListEntry" />
|
||||||
</data>
|
</data>
|
||||||
@@ -76,11 +77,17 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingBottom="8dp"
|
||||||
|
android:visibility="@{(!advanced || nonInteractive) ? View.VISIBLE : View.GONE}" />
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:padding="8dp"
|
android:padding="8dp"
|
||||||
android:visibility="@{nonInteractive ? View.GONE : View.VISIBLE}">
|
android:visibility="@{(!advanced || nonInteractive) ? View.GONE : View.VISIBLE}">
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/import_key"
|
android:id="@+id/import_key"
|
||||||
@@ -109,20 +116,27 @@
|
|||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/extra_container"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:animateLayoutChanges="true"
|
android:visibility="@{advanced ? View.VISIBLE : View.GONE}">
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingBottom="24dp"
|
|
||||||
android:paddingLeft="16dp"
|
|
||||||
android:paddingRight="16dp"
|
|
||||||
android:paddingTop="16dp"
|
|
||||||
android:visibility="gone">
|
|
||||||
|
|
||||||
<include
|
<LinearLayout
|
||||||
layout="@layout/import_keys_list_item_extra"
|
android:id="@+id/extra_container"
|
||||||
app:entry="@{entry}" />
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:animateLayoutChanges="true"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="24dp"
|
||||||
|
android:paddingLeft="16dp"
|
||||||
|
android:paddingRight="16dp"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<include
|
||||||
|
layout="@layout/import_keys_list_item_extra"
|
||||||
|
app:entry="@{entry}" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
12
OpenKeychain/src/main/res/menu/import_keys_list_fragment.xml
Normal file
12
OpenKeychain/src/main/res/menu/import_keys_list_fragment.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/basic"
|
||||||
|
android:title="@string/menu_basic" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/advanced"
|
||||||
|
android:title="@string/menu_advanced" />
|
||||||
|
|
||||||
|
</menu>
|
||||||
@@ -128,6 +128,7 @@
|
|||||||
<string name="menu_select_all">"Select all"</string>
|
<string name="menu_select_all">"Select all"</string>
|
||||||
<string name="menu_export_all_keys">"Export all keys"</string>
|
<string name="menu_export_all_keys">"Export all keys"</string>
|
||||||
<string name="menu_update_all_keys">"Update all keys"</string>
|
<string name="menu_update_all_keys">"Update all keys"</string>
|
||||||
|
<string name="menu_basic">"Basic"</string>
|
||||||
<string name="menu_advanced">"Advanced"</string>
|
<string name="menu_advanced">"Advanced"</string>
|
||||||
<string name="menu_certify_fingerprint">"Confirm with fingerprint"</string>
|
<string name="menu_certify_fingerprint">"Confirm with fingerprint"</string>
|
||||||
<string name="menu_certify_fingerprint_phrases">"Confirm with phrases"</string>
|
<string name="menu_certify_fingerprint_phrases">"Confirm with phrases"</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user