Merge "Migrate functionality to Me Card fragment."
This commit is contained in:
committed by
Android (Google) Code Review
commit
b04556453a
@@ -1011,7 +1011,7 @@
|
||||
android:taskAffinity="com.android.settings"
|
||||
android:parentActivityName="Settings">
|
||||
<intent-filter android:priority="1">
|
||||
<action android:name="android.settings.DEVICE_HEALTH_SETTINGS" />
|
||||
<action android:name="android.settings.DEVICE_INFO_SETTINGS" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
@@ -1027,7 +1027,7 @@
|
||||
<meta-data android:name="com.android.settings.title"
|
||||
android:resource="@string/about_settings" />
|
||||
<meta-data android:name="com.android.settings.FRAGMENT_CLASS"
|
||||
android:value="com.android.settings.MeCardFragment" />
|
||||
android:value="com.android.settings.deviceinfo.aboutphone.MyDeviceInfoFragment" />
|
||||
<meta-data android:name="com.android.settings.PRIMARY_PROFILE_CONTROLLED"
|
||||
android:value="true" />
|
||||
</activity>
|
||||
|
@@ -6445,6 +6445,7 @@
|
||||
<string name="help_uri_process_stats_summary" translatable="false"></string>
|
||||
<string name="help_uri_process_stats_apps" translatable="false"></string>
|
||||
<string name="help_uri_private_dns" translatable="false"></string>
|
||||
<string name="help_uri_about_phone_v2" translatable="false"></string>
|
||||
|
||||
<!-- User account title [CHAR LIMIT=30] -->
|
||||
<string name="user_account_title">Account for content</string>
|
||||
@@ -9231,14 +9232,14 @@
|
||||
<string name="account_confirmation_package"></string>
|
||||
|
||||
<!-- Title for the new About Phone screen [CHAR LIMIT=40] -->
|
||||
<string name="me_card_title" product="default">My Phone</string>
|
||||
<string name="my_device_info_title" product="default">My Phone</string>
|
||||
<!-- Title for the new About Phone screen [CHAR LIMIT=40] -->
|
||||
<string name="me_card_title" product="tablet">My Tablet</string>
|
||||
<string name="my_device_info_title" product="tablet">My Tablet</string>
|
||||
<!-- Title for the new About Phone screen [CHAR LIMIT=40] -->
|
||||
<string name="me_card_title" product="device">My Device</string>
|
||||
<string name="my_device_info_title" product="device">My Device</string>
|
||||
<!-- Title for preference showing the primary account on the device [CHAR LIMIT=60]-->
|
||||
<string name="me_card_account_preference_title">Account</string>
|
||||
<string name="my_device_info_account_preference_title">Account</string>
|
||||
<!-- Title for preference showing the name of the device. [CHAR LIMIT=60]-->
|
||||
<string name="me_card_device_name_preference_title">Device name</string>
|
||||
<string name="my_device_info_device_name_preference_title">Device name</string>
|
||||
|
||||
</resources>
|
||||
|
@@ -1,50 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2018 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:key="me_card_pref_screen"
|
||||
android:title="@string/me_card_title">
|
||||
|
||||
<com.android.settings.applications.LayoutPreference
|
||||
android:key="me_card_header"
|
||||
android:order="0"
|
||||
android:layout="@layout/settings_entity_header"
|
||||
android:selectable="false"/>
|
||||
|
||||
<!-- Account name -->
|
||||
<Preference
|
||||
android:key="account"
|
||||
android:order="1"
|
||||
android:title="@string/me_card_account_preference_title"
|
||||
android:summary="@string/summary_placeholder"/>
|
||||
|
||||
<!-- Phone number -->
|
||||
<Preference
|
||||
android:key="phone_number"
|
||||
android:order="2"
|
||||
android:title="@string/status_number"
|
||||
android:summary="@string/summary_placeholder"/>
|
||||
|
||||
<!-- Device name -->
|
||||
<Preference
|
||||
android:key="device_name"
|
||||
android:order="3"
|
||||
android:title="@string/me_card_device_name_preference_title"
|
||||
android:summary="@string/summary_placeholder"/>
|
||||
|
||||
</PreferenceScreen>
|
167
res/xml/my_device_info.xml
Normal file
167
res/xml/my_device_info.xml
Normal file
@@ -0,0 +1,167 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2018 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:settings="http://schemas.android.com/apk/res-auto"
|
||||
android:key="my_device_info_pref_screen"
|
||||
android:title="@string/my_device_info_title"
|
||||
settings:initialExpandedChildrenCount="4">
|
||||
|
||||
<com.android.settings.applications.LayoutPreference
|
||||
android:key="my_device_info_header"
|
||||
android:order="0"
|
||||
android:layout="@layout/settings_entity_header"
|
||||
android:selectable="false"/>
|
||||
|
||||
<!-- Account name -->
|
||||
<Preference
|
||||
android:key="account"
|
||||
android:order="1"
|
||||
android:title="@string/my_device_info_account_preference_title"
|
||||
android:summary="@string/summary_placeholder"/>
|
||||
|
||||
<!-- Phone number -->
|
||||
<Preference
|
||||
android:key="phone_number"
|
||||
android:order="2"
|
||||
android:title="@string/status_number"
|
||||
android:summary="@string/summary_placeholder"/>
|
||||
|
||||
<!-- Device name -->
|
||||
<Preference
|
||||
android:key="device_name"
|
||||
android:order="3"
|
||||
android:title="@string/my_device_info_device_name_preference_title"
|
||||
android:summary="@string/summary_placeholder"/>
|
||||
|
||||
<!-- SIM status -->
|
||||
<Preference
|
||||
android:key="sim_status"
|
||||
android:order="4"
|
||||
android:title="@string/sim_status_title"
|
||||
settings:keywords="@string/keywords_sim_status"
|
||||
android:summary="@string/summary_placeholder"/>
|
||||
|
||||
<!-- Model & hardware -->
|
||||
<Preference
|
||||
android:key="device_model"
|
||||
android:order="21"
|
||||
android:title="@string/hardware_info"
|
||||
settings:keywords="@string/keywords_model_and_hardware"
|
||||
android:summary="@string/summary_placeholder"/>
|
||||
|
||||
<!-- IMEI -->
|
||||
<Preference
|
||||
android:key="imei_info"
|
||||
android:order="22"
|
||||
android:title="@string/status_imei"
|
||||
settings:keywords="@string/keywords_imei_info"
|
||||
android:summary="@string/summary_placeholder"/>
|
||||
|
||||
<!-- Android version -->
|
||||
<Preference
|
||||
android:key="firmware_version"
|
||||
android:order="32"
|
||||
android:title="@string/firmware_version"
|
||||
settings:keywords="@string/keywords_android_version"
|
||||
android:summary="@string/summary_placeholder"/>
|
||||
|
||||
<!-- IMS registration -->
|
||||
<Preference
|
||||
android:key="ims_reg_state"
|
||||
android:order="33"
|
||||
android:title="@string/ims_reg_title"
|
||||
android:summary="@string/summary_placeholder"/>
|
||||
|
||||
<!--IP address -->
|
||||
<Preference
|
||||
android:key="wifi_ip_address"
|
||||
android:order="34"
|
||||
android:title="@string/wifi_ip_address"
|
||||
android:summary="@string/summary_placeholder"
|
||||
settings:allowDividerAbove="true"/>
|
||||
|
||||
<!-- Wi-Fi MAC address -->
|
||||
<Preference
|
||||
android:key="wifi_mac_address"
|
||||
android:order="35"
|
||||
android:title="@string/status_wifi_mac_address"
|
||||
android:summary="@string/summary_placeholder"/>
|
||||
|
||||
<!-- Bluetooth address -->
|
||||
<Preference
|
||||
android:key="bt_address"
|
||||
android:order="36"
|
||||
android:title="@string/status_bt_address"
|
||||
android:summary="@string/summary_placeholder"/>
|
||||
|
||||
|
||||
<!-- Legal information -->
|
||||
<Preference
|
||||
android:key="legal_container"
|
||||
android:order="37"
|
||||
android:title="@string/legal_information"
|
||||
android:fragment="com.android.settings.LegalSettings"
|
||||
settings:allowDividerAbove="true"/>
|
||||
|
||||
<!-- Regulatory labels -->
|
||||
<Preference
|
||||
android:key="regulatory_info"
|
||||
android:order="38"
|
||||
android:title="@string/regulatory_labels">
|
||||
<intent android:action="android.settings.SHOW_REGULATORY_INFO"/>
|
||||
</Preference>
|
||||
|
||||
<!-- Safety & regulatory manual -->
|
||||
<Preference
|
||||
android:key="safety_info"
|
||||
android:order="39"
|
||||
android:title="@string/safety_and_regulatory_info">
|
||||
<intent android:action="android.settings.SHOW_SAFETY_AND_REGULATORY_INFO"/>
|
||||
</Preference>
|
||||
|
||||
<!-- Manual -->
|
||||
<Preference
|
||||
android:key="manual"
|
||||
android:order="40"
|
||||
android:title="@string/manual">
|
||||
<intent android:action="android.settings.SHOW_MANUAL"/>
|
||||
</Preference>
|
||||
|
||||
<!-- Feedback on the device -->
|
||||
<Preference
|
||||
android:key="device_feedback"
|
||||
android:order="41"
|
||||
android:title="@string/device_feedback"/>
|
||||
|
||||
<!-- Device FCC equipment id -->
|
||||
<Preference
|
||||
android:key="fcc_equipment_id"
|
||||
android:order="42"
|
||||
android:title="@string/fcc_equipment_id"
|
||||
android:summary="@string/summary_placeholder"/>
|
||||
|
||||
<!-- Build number -->
|
||||
<Preference
|
||||
android:key="build_number"
|
||||
android:order="43"
|
||||
android:title="@string/build_number"
|
||||
android:summary="@string/summary_placeholder"
|
||||
settings:allowDividerAbove="true"/>
|
||||
|
||||
</PreferenceScreen>
|
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settings;
|
||||
package com.android.settings.deviceinfo.aboutphone;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
@@ -23,13 +23,32 @@ import android.content.pm.UserInfo;
|
||||
import android.os.Bundle;
|
||||
import android.os.UserManager;
|
||||
import android.provider.SearchIndexableResource;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.view.View;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.SettingsActivity;
|
||||
import com.android.settings.Utils;
|
||||
import com.android.settings.applications.LayoutPreference;
|
||||
import com.android.settings.dashboard.DashboardFragment;
|
||||
import com.android.settings.deviceinfo.BluetoothAddressPreferenceController;
|
||||
import com.android.settings.deviceinfo.BrandedAccountPreferenceController;
|
||||
import com.android.settings.deviceinfo.BuildNumberPreferenceController;
|
||||
import com.android.settings.deviceinfo.DeviceModelPreferenceController;
|
||||
import com.android.settings.deviceinfo.FccEquipmentIdPreferenceController;
|
||||
import com.android.settings.deviceinfo.FeedbackPreferenceController;
|
||||
import com.android.settings.deviceinfo.ImsStatusPreferenceController;
|
||||
import com.android.settings.deviceinfo.IpAddressPreferenceController;
|
||||
import com.android.settings.deviceinfo.ManualPreferenceController;
|
||||
import com.android.settings.deviceinfo.PhoneNumberPreferenceController;
|
||||
import com.android.settings.deviceinfo.RegulatoryInfoPreferenceController;
|
||||
import com.android.settings.deviceinfo.SafetyInfoPreferenceController;
|
||||
import com.android.settings.deviceinfo.WifiMacAddressPreferenceController;
|
||||
import com.android.settings.deviceinfo.firmwareversion.FirmwareVersionPreferenceController;
|
||||
import com.android.settings.deviceinfo.imei.ImeiInfoPreferenceController;
|
||||
import com.android.settings.deviceinfo.simstatus.SimStatusPreferenceController;
|
||||
import com.android.settings.search.BaseSearchIndexProvider;
|
||||
import com.android.settings.search.Indexable;
|
||||
import com.android.settings.widget.EntityHeaderController;
|
||||
@@ -40,10 +59,11 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class MeCardFragment extends DashboardFragment implements Indexable {
|
||||
public class MyDeviceInfoFragment extends DashboardFragment {
|
||||
private static final String LOG_TAG = "MeCardFragment";
|
||||
|
||||
private static final String KEY_ME_CARD_HEADER = "me_card_header";
|
||||
private static final String KEY_MY_DEVICE_INFO_HEADER = "my_device_info_header";
|
||||
private static final String KEY_LEGAL_CONTAINER = "legal_container";
|
||||
|
||||
@Override
|
||||
public int getMetricsCategory() {
|
||||
@@ -68,7 +88,7 @@ public class MeCardFragment extends DashboardFragment implements Indexable {
|
||||
|
||||
@Override
|
||||
protected int getPreferenceScreenResId() {
|
||||
return R.xml.me_card;
|
||||
return R.xml.my_device_info;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -82,6 +102,21 @@ public class MeCardFragment extends DashboardFragment implements Indexable {
|
||||
final List<AbstractPreferenceController> controllers = new ArrayList<>();
|
||||
controllers.add(new PhoneNumberPreferenceController(context));
|
||||
controllers.add(new BrandedAccountPreferenceController(context));
|
||||
controllers.add(new SimStatusPreferenceController(context, fragment));
|
||||
controllers.add(new DeviceModelPreferenceController(context, fragment));
|
||||
controllers.add(new ImeiInfoPreferenceController(context, fragment));
|
||||
controllers.add(new FirmwareVersionPreferenceController(context, fragment));
|
||||
controllers.add(new ImsStatusPreferenceController(context, lifecycle));
|
||||
controllers.add(new IpAddressPreferenceController(context, lifecycle));
|
||||
controllers.add(new WifiMacAddressPreferenceController(context, lifecycle));
|
||||
controllers.add(new BluetoothAddressPreferenceController(context, lifecycle));
|
||||
controllers.add(new RegulatoryInfoPreferenceController(context));
|
||||
controllers.add(new SafetyInfoPreferenceController(context));
|
||||
controllers.add(new ManualPreferenceController(context));
|
||||
controllers.add(new FeedbackPreferenceController(fragment, context));
|
||||
controllers.add(new FccEquipmentIdPreferenceController(context));
|
||||
controllers.add(
|
||||
new BuildNumberPreferenceController(context, activity, fragment, lifecycle));
|
||||
// TODO: Add preference controller for getting the device name.
|
||||
return controllers;
|
||||
}
|
||||
@@ -89,7 +124,7 @@ public class MeCardFragment extends DashboardFragment implements Indexable {
|
||||
private void initHeader() {
|
||||
// TODO: Migrate into its own controller.
|
||||
final LayoutPreference headerPreference =
|
||||
(LayoutPreference) getPreferenceScreen().findPreference(KEY_ME_CARD_HEADER);
|
||||
(LayoutPreference) getPreferenceScreen().findPreference(KEY_MY_DEVICE_INFO_HEADER);
|
||||
final View appSnippet = headerPreference.findViewById(R.id.entity_header);
|
||||
final Activity context = getActivity();
|
||||
final Bundle bundle = getArguments();
|
||||
@@ -123,7 +158,7 @@ public class MeCardFragment extends DashboardFragment implements Indexable {
|
||||
public List<SearchIndexableResource> getXmlResourcesToIndex(
|
||||
Context context, boolean enabled) {
|
||||
final SearchIndexableResource sir = new SearchIndexableResource(context);
|
||||
sir.xmlResId = R.xml.me_card;
|
||||
sir.xmlResId = R.xml.my_device_info;
|
||||
return Arrays.asList(sir);
|
||||
}
|
||||
|
||||
@@ -133,5 +168,13 @@ public class MeCardFragment extends DashboardFragment implements Indexable {
|
||||
return buildPreferenceControllers(context, null /*activity */,
|
||||
null /* fragment */, null /* lifecycle */);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getNonIndexableKeys(Context context) {
|
||||
List<String> keys = super.getNonIndexableKeys(context);
|
||||
// The legal container is duplicated, so we ignore it here.
|
||||
keys.add(KEY_LEGAL_CONTAINER);
|
||||
return keys;
|
||||
}
|
||||
};
|
||||
}
|
@@ -21,7 +21,7 @@ import android.support.annotation.VisibleForTesting;
|
||||
import com.android.settings.DateTimeSettings;
|
||||
import com.android.settings.DisplaySettings;
|
||||
import com.android.settings.LegalSettings;
|
||||
import com.android.settings.MeCardFragment;
|
||||
import com.android.settings.deviceinfo.aboutphone.MyDeviceInfoFragment;
|
||||
import com.android.settings.accessibility.AccessibilitySettings;
|
||||
import com.android.settings.accessibility.AccessibilityShortcutPreferenceFragment;
|
||||
import com.android.settings.accessibility.MagnificationPreferenceFragment;
|
||||
@@ -174,7 +174,7 @@ public class SearchIndexableResourcesImpl implements SearchIndexableResources {
|
||||
addIndex(ZenModeAutomationSettings.class);
|
||||
addIndex(NightDisplaySettings.class);
|
||||
addIndex(SmartBatterySettings.class);
|
||||
addIndex(MeCardFragment.class);
|
||||
addIndex(MyDeviceInfoFragment.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settings.deviceinfo;
|
||||
|
||||
import static com.android.settings.SettingsActivity.EXTRA_FRAGMENT_ARG_KEY;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.preference.PreferenceScreen;
|
||||
import android.telephony.TelephonyManager;
|
||||
|
||||
import com.android.settings.deviceinfo.aboutphone.MyDeviceInfoFragment;
|
||||
import com.android.settings.TestConfig;
|
||||
import com.android.settings.testutils.FakeFeatureFactory;
|
||||
import com.android.settings.testutils.SettingsRobolectricTestRunner;
|
||||
import com.android.settings.testutils.shadow.SettingsShadowResources;
|
||||
import com.android.settings.testutils.shadow.SettingsShadowSystemProperties;
|
||||
import com.android.settings.testutils.shadow.ShadowConnectivityManager;
|
||||
import com.android.settings.testutils.shadow.ShadowUserManager;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.shadows.ShadowApplication;
|
||||
|
||||
@RunWith(SettingsRobolectricTestRunner.class)
|
||||
@Config(
|
||||
manifest = TestConfig.MANIFEST_PATH,
|
||||
sdk = TestConfig.SDK_VERSION,
|
||||
shadows = {ShadowConnectivityManager.class, ShadowUserManager.class}
|
||||
)
|
||||
public class MyDeviceInfoFragmentTest {
|
||||
@Mock
|
||||
private Activity mActivity;
|
||||
@Mock
|
||||
private PreferenceScreen mScreen;
|
||||
@Mock
|
||||
private TelephonyManager mTelephonyManager;
|
||||
|
||||
private Context mContext;
|
||||
private MyDeviceInfoFragment mSettings;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
FakeFeatureFactory.setupForTest();
|
||||
mContext = RuntimeEnvironment.application;
|
||||
mSettings = spy(new MyDeviceInfoFragment());
|
||||
|
||||
when(mSettings.getActivity()).thenReturn(mActivity);
|
||||
when(mSettings.getContext()).thenReturn(mContext);
|
||||
when(mActivity.getTheme()).thenReturn(mContext.getTheme());
|
||||
when(mActivity.getResources()).thenReturn(mContext.getResources());
|
||||
doNothing().when(mSettings).onCreatePreferences(any(), any());
|
||||
|
||||
doReturn(mScreen).when(mSettings).getPreferenceScreen();
|
||||
when(mSettings.getPreferenceScreen()).thenReturn(mScreen);
|
||||
ShadowApplication.getInstance().setSystemService(Context.TELEPHONY_SERVICE,
|
||||
mTelephonyManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(shadows = {SettingsShadowResources.SettingsShadowTheme.class,
|
||||
SettingsShadowSystemProperties.class})
|
||||
public void onCreate_fromSearch_shouldNotOverrideInitialExpandedCount() {
|
||||
final Bundle args = new Bundle();
|
||||
args.putString(EXTRA_FRAGMENT_ARG_KEY, "search_key");
|
||||
mSettings.setArguments(args);
|
||||
|
||||
mSettings.onCreate(null /* icicle */);
|
||||
|
||||
verify(mScreen).setInitialExpandedChildrenCount(Integer.MAX_VALUE);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user