Snap for 10078820 from 17a0266b16
to udc-release
Change-Id: I9e5dab9eaa426371cdc64e4e517846863d1cafe8
This commit is contained in:
@@ -17,9 +17,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:pathData="M20,6h-4L16,4c0,-1.11 -0.89,-2 -2,-2h-4c-1.11,0 -2,0.89 -2,2v2L4,6c-1.11,0 -1.99,0.89 -1.99,2L2,19c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2zM12,15c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM14,6h-4L10,4h4v2z"
|
||||
android:pathData="M140,840Q116,840 98,822Q80,804 80,780L80,300Q80,276 98,258Q116,240 140,240L320,240L320,140Q320,116 338,98Q356,80 380,80L580,80Q604,80 622,98Q640,116 640,140L640,240L820,240Q844,240 862,258Q880,276 880,300L880,780Q880,804 862,822Q844,840 820,840L140,840ZM380,240L580,240L580,140Q580,140 580,140Q580,140 580,140L380,140Q380,140 380,140Q380,140 380,140L380,240Z"
|
||||
android:fillColor="?android:attr/colorPrimary"/>
|
||||
</vector>
|
26
res/drawable/ic_stylus.xml
Normal file
26
res/drawable/ic_stylus.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<!--
|
||||
~ Copyright (C) 2023 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.
|
||||
-->
|
||||
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?android:attr/colorControlNormal">
|
||||
<path android:fillColor="@android:color/white"
|
||||
android:pathData="M167,840Q146,845 130.5,829.5Q115,814 120,793L160,602L358,800L167,840ZM358,800L160,602L618,144Q641,121 675,121Q709,121 732,144L816,228Q839,251 839,285Q839,319 816,342L358,800ZM675,200L261,614L346,699L760,285Q760,285 760,285Q760,285 760,285L675,200Q675,200 675,200Q675,200 675,200Z"/>
|
||||
</vector>
|
@@ -43,18 +43,22 @@ public final class CombinedProviderInfo {
|
||||
private final List<CredentialProviderInfo> mCredentialProviderInfos;
|
||||
private final @Nullable AutofillServiceInfo mAutofillServiceInfo;
|
||||
private final boolean mIsDefaultAutofillProvider;
|
||||
private final boolean mIsDefaultCredmanProvider;
|
||||
private final boolean mIsPrimaryCredmanProvider;
|
||||
|
||||
/** Constructs an information instance from both autofill and credential provider. */
|
||||
public CombinedProviderInfo(
|
||||
@Nullable List<CredentialProviderInfo> cpis,
|
||||
@Nullable AutofillServiceInfo asi,
|
||||
boolean isDefaultAutofillProvider,
|
||||
boolean isDefaultCredmanProvider) {
|
||||
boolean IsPrimaryCredmanProvider) {
|
||||
if (cpis == null) {
|
||||
mCredentialProviderInfos = new ArrayList<>();
|
||||
} else {
|
||||
mCredentialProviderInfos = new ArrayList<>(cpis);
|
||||
}
|
||||
mAutofillServiceInfo = asi;
|
||||
mIsDefaultAutofillProvider = isDefaultAutofillProvider;
|
||||
mIsDefaultCredmanProvider = isDefaultCredmanProvider;
|
||||
mIsPrimaryCredmanProvider = IsPrimaryCredmanProvider;
|
||||
}
|
||||
|
||||
/** Returns the credential provider info. */
|
||||
@@ -149,8 +153,8 @@ public final class CombinedProviderInfo {
|
||||
}
|
||||
|
||||
/** Returns whether the provider is the default credman provider. */
|
||||
public boolean isDefaultCredmanProvider() {
|
||||
return mIsDefaultCredmanProvider;
|
||||
public boolean isPrimaryCredmanProvider() {
|
||||
return mIsPrimaryCredmanProvider;
|
||||
}
|
||||
|
||||
/** Returns the settings subtitle. */
|
||||
@@ -192,7 +196,13 @@ public final class CombinedProviderInfo {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(280454916): Add logic here.
|
||||
// If there is a primary cred man provider then return that.
|
||||
for (CombinedProviderInfo cpi : providers) {
|
||||
if (cpi.isPrimaryCredmanProvider()) {
|
||||
return cpi;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -250,14 +260,14 @@ public final class CombinedProviderInfo {
|
||||
}
|
||||
|
||||
// Check if we have any enabled cred man services.
|
||||
boolean isDefaultCredmanProvider = false;
|
||||
if (!cpi.isEmpty()) {
|
||||
isDefaultCredmanProvider = cpi.get(0).isEnabled();
|
||||
boolean isPrimaryCredmanProvider = false;
|
||||
if (cpi != null && !cpi.isEmpty()) {
|
||||
isPrimaryCredmanProvider = cpi.get(0).isPrimary();
|
||||
}
|
||||
|
||||
cmpi.add(
|
||||
new CombinedProviderInfo(
|
||||
cpi, selectedAsi, isDefaultAutofillProvider, isDefaultCredmanProvider));
|
||||
cpi, selectedAsi, isDefaultAutofillProvider, isPrimaryCredmanProvider));
|
||||
}
|
||||
|
||||
return cmpi;
|
||||
|
@@ -120,6 +120,7 @@ public class CredentialManagerPreferenceController extends BasePreferenceControl
|
||||
mCredentialManager =
|
||||
getCredentialManager(context, preferenceKey.equals("credentials_test"));
|
||||
new SettingContentObserver(mHandler).register(context.getContentResolver());
|
||||
mSettingsPackageMonitor.register(context, context.getMainLooper(), false);
|
||||
}
|
||||
|
||||
private @Nullable CredentialManager getCredentialManager(Context context, boolean isTest) {
|
||||
@@ -321,7 +322,7 @@ public class CredentialManagerPreferenceController extends BasePreferenceControl
|
||||
|
||||
mEnabledPackageNames.clear();
|
||||
for (CredentialProviderInfo cpi : availableServices) {
|
||||
if (cpi.isEnabled()) {
|
||||
if (cpi.isEnabled() && !cpi.isPrimary()) {
|
||||
mEnabledPackageNames.add(cpi.getServiceInfo().packageName);
|
||||
}
|
||||
}
|
||||
@@ -560,15 +561,25 @@ public class CredentialManagerPreferenceController extends BasePreferenceControl
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> enabledServices = getEnabledSettings();
|
||||
// Get the existing primary providers since we don't touch them in
|
||||
// this part of the UI we should just copy them over.
|
||||
Set<String> primaryServices = new HashSet<>();
|
||||
for (CredentialProviderInfo service : mServices) {
|
||||
if (service.isPrimary()) {
|
||||
primaryServices.add(service.getServiceInfo().getComponentName().flattenToString());
|
||||
}
|
||||
}
|
||||
|
||||
mCredentialManager.setEnabledProviders(
|
||||
enabledServices,
|
||||
new ArrayList<>(primaryServices),
|
||||
getEnabledSettings(),
|
||||
getUser(),
|
||||
mExecutor,
|
||||
new OutcomeReceiver<Void, SetEnabledProvidersException>() {
|
||||
@Override
|
||||
public void onResult(Void result) {
|
||||
Log.i(TAG, "setEnabledProviders success");
|
||||
updateFromExternal();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -47,7 +47,9 @@ import com.android.settingslib.utils.ThreadUtils;
|
||||
import com.android.settingslib.widget.CandidateInfo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class DefaultCombinedPicker extends DefaultAppPickerFragment {
|
||||
|
||||
@@ -338,9 +340,9 @@ public class DefaultCombinedPicker extends DefaultAppPickerFragment {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void setProviders(String autofillProvider, List<String> credManProviders) {
|
||||
private void setProviders(String autofillProvider, List<String> primaryCredManProviders) {
|
||||
if (TextUtils.isEmpty(autofillProvider)) {
|
||||
if (credManProviders.size() > 0) {
|
||||
if (primaryCredManProviders.size() > 0) {
|
||||
autofillProvider =
|
||||
CredentialManagerPreferenceController
|
||||
.AUTOFILL_CREDMAN_ONLY_PROVIDER_PLACEHOLDER;
|
||||
@@ -350,12 +352,25 @@ public class DefaultCombinedPicker extends DefaultAppPickerFragment {
|
||||
Settings.Secure.putStringForUser(
|
||||
getContext().getContentResolver(), AUTOFILL_SETTING, autofillProvider, mUserId);
|
||||
|
||||
CredentialManager service = getCredentialProviderService();
|
||||
final CredentialManager service = getCredentialProviderService();
|
||||
if (service == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the existing secondary providers since we don't touch them in
|
||||
// this part of the UI we should just copy them over.
|
||||
final List<String> credManProviders = new ArrayList<>();
|
||||
for (CredentialProviderInfo cpi :
|
||||
service.getCredentialProviderServices(
|
||||
mUserId, CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_ONLY)) {
|
||||
|
||||
if (cpi.isEnabled()) {
|
||||
credManProviders.add(cpi.getServiceInfo().getComponentName().flattenToString());
|
||||
}
|
||||
}
|
||||
|
||||
service.setEnabledProviders(
|
||||
primaryCredManProviders,
|
||||
credManProviders,
|
||||
mUserId,
|
||||
ContextCompat.getMainExecutor(getContext()),
|
||||
|
@@ -135,12 +135,12 @@ public class DefaultCombinedPreferenceController extends DefaultAppPreferenceCon
|
||||
/** Provides Intent to setting activity for the specified autofill service. */
|
||||
static final class AutofillSettingIntentProvider {
|
||||
|
||||
private final String mSelectedKey;
|
||||
private final String mKey;
|
||||
private final Context mContext;
|
||||
private final int mUserId;
|
||||
|
||||
public AutofillSettingIntentProvider(Context context, int userId, String key) {
|
||||
mSelectedKey = key;
|
||||
mKey = key;
|
||||
mContext = context;
|
||||
mUserId = userId;
|
||||
}
|
||||
@@ -153,10 +153,9 @@ public class DefaultCombinedPreferenceController extends DefaultAppPreferenceCon
|
||||
|
||||
for (ResolveInfo resolveInfo : resolveInfos) {
|
||||
final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
|
||||
final String flattenKey =
|
||||
new ComponentName(serviceInfo.packageName, serviceInfo.name)
|
||||
.flattenToString();
|
||||
if (TextUtils.equals(mSelectedKey, flattenKey)) {
|
||||
|
||||
// If there are multiple autofill services then pick the first one.
|
||||
if (mKey.startsWith(serviceInfo.packageName)) {
|
||||
final String settingsActivity;
|
||||
try {
|
||||
settingsActivity =
|
||||
@@ -164,7 +163,7 @@ public class DefaultCombinedPreferenceController extends DefaultAppPreferenceCon
|
||||
.getSettingsActivity();
|
||||
} catch (SecurityException e) {
|
||||
// Service does not declare the proper permission, ignore it.
|
||||
Log.w(TAG, "Error getting info for " + serviceInfo + ": " + e);
|
||||
Log.e(TAG, "Error getting info for " + serviceInfo + ": " + e);
|
||||
return null;
|
||||
}
|
||||
if (TextUtils.isEmpty(settingsActivity)) {
|
||||
|
@@ -72,6 +72,7 @@ public class RequestPermissionActivity extends Activity implements
|
||||
private int mRequest;
|
||||
|
||||
private AlertDialog mDialog;
|
||||
private AlertDialog mRequestDialog;
|
||||
|
||||
private BroadcastReceiver mReceiver;
|
||||
|
||||
@@ -96,12 +97,12 @@ public class RequestPermissionActivity extends Activity implements
|
||||
if (mRequest == REQUEST_DISABLE) {
|
||||
switch (btState) {
|
||||
case BluetoothAdapter.STATE_OFF:
|
||||
case BluetoothAdapter.STATE_TURNING_OFF: {
|
||||
case BluetoothAdapter.STATE_TURNING_OFF:
|
||||
proceedAndFinish();
|
||||
} break;
|
||||
|
||||
break;
|
||||
case BluetoothAdapter.STATE_ON:
|
||||
case BluetoothAdapter.STATE_TURNING_ON: {
|
||||
case BluetoothAdapter.STATE_TURNING_ON:
|
||||
mRequestDialog =
|
||||
RequestPermissionHelper.INSTANCE.requestDisable(this, mAppLabel,
|
||||
() -> {
|
||||
onDisableConfirmed();
|
||||
@@ -111,18 +112,20 @@ public class RequestPermissionActivity extends Activity implements
|
||||
cancelAndFinish();
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
} break;
|
||||
|
||||
default: {
|
||||
if (mRequestDialog != null) {
|
||||
mRequestDialog.show();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Log.e(TAG, "Unknown adapter state: " + btState);
|
||||
cancelAndFinish();
|
||||
} break;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
switch (btState) {
|
||||
case BluetoothAdapter.STATE_OFF:
|
||||
case BluetoothAdapter.STATE_TURNING_OFF:
|
||||
case BluetoothAdapter.STATE_TURNING_ON: {
|
||||
case BluetoothAdapter.STATE_TURNING_ON:
|
||||
/*
|
||||
* Strictly speaking STATE_TURNING_ON belong with STATE_ON;
|
||||
* however, BT may not be ready when the user clicks yes and we
|
||||
@@ -131,7 +134,8 @@ public class RequestPermissionActivity extends Activity implements
|
||||
* case via the broadcast receiver.
|
||||
*/
|
||||
|
||||
// Start the helper activity to ask the user about enabling bt AND discovery
|
||||
// Show the helper dialog to ask the user about enabling bt AND discovery
|
||||
mRequestDialog =
|
||||
RequestPermissionHelper.INSTANCE.requestEnable(this, mAppLabel,
|
||||
mRequest == REQUEST_ENABLE_DISCOVERABLE ? mTimeout : -1,
|
||||
() -> {
|
||||
@@ -142,9 +146,11 @@ public class RequestPermissionActivity extends Activity implements
|
||||
cancelAndFinish();
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
} break;
|
||||
|
||||
case BluetoothAdapter.STATE_ON: {
|
||||
if (mRequestDialog != null) {
|
||||
mRequestDialog.show();
|
||||
}
|
||||
break;
|
||||
case BluetoothAdapter.STATE_ON:
|
||||
if (mRequest == REQUEST_ENABLE) {
|
||||
// Nothing to do. Already enabled.
|
||||
proceedAndFinish();
|
||||
@@ -152,12 +158,11 @@ public class RequestPermissionActivity extends Activity implements
|
||||
// Ask the user about enabling discovery mode
|
||||
createDialog();
|
||||
}
|
||||
} break;
|
||||
|
||||
default: {
|
||||
break;
|
||||
default:
|
||||
Log.e(TAG, "Unknown adapter state: " + btState);
|
||||
cancelAndFinish();
|
||||
} break;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,10 +280,6 @@ public class RequestPermissionActivity extends Activity implements
|
||||
}
|
||||
}
|
||||
|
||||
if (mDialog != null) {
|
||||
mDialog.dismiss();
|
||||
}
|
||||
|
||||
setResult(returnCode);
|
||||
finish();
|
||||
}
|
||||
@@ -365,6 +366,14 @@ public class RequestPermissionActivity extends Activity implements
|
||||
unregisterReceiver(mReceiver);
|
||||
mReceiver = null;
|
||||
}
|
||||
if (mDialog != null && mDialog.isShowing()) {
|
||||
mDialog.dismiss();
|
||||
mDialog = null;
|
||||
}
|
||||
if (mRequestDialog != null && mRequestDialog.isShowing()) {
|
||||
mRequestDialog.dismiss();
|
||||
mRequestDialog = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -30,20 +30,20 @@ object RequestPermissionHelper {
|
||||
timeout: Int,
|
||||
onAllow: () -> Unit,
|
||||
onDeny: () -> Unit,
|
||||
) {
|
||||
): AlertDialog? {
|
||||
if (context.resources.getBoolean(R.bool.auto_confirm_bluetooth_activation_dialog)) {
|
||||
// Don't even show the dialog if configured this way
|
||||
onAllow()
|
||||
return
|
||||
return null
|
||||
}
|
||||
AlertDialog.Builder(context).apply {
|
||||
return AlertDialog.Builder(context).apply {
|
||||
setMessage(context.getEnableMessage(timeout, appLabel))
|
||||
setPositiveButton(R.string.allow) { _, _ ->
|
||||
if (context.isDisallowBluetooth()) onDeny() else onAllow()
|
||||
}
|
||||
setNegativeButton(R.string.deny) { _, _ -> onDeny() }
|
||||
setOnCancelListener { onDeny() }
|
||||
}.show()
|
||||
}.create()
|
||||
}
|
||||
|
||||
fun requestDisable(
|
||||
@@ -51,18 +51,18 @@ object RequestPermissionHelper {
|
||||
appLabel: CharSequence?,
|
||||
onAllow: () -> Unit,
|
||||
onDeny: () -> Unit,
|
||||
) {
|
||||
): AlertDialog? {
|
||||
if (context.resources.getBoolean(R.bool.auto_confirm_bluetooth_activation_dialog)) {
|
||||
// Don't even show the dialog if configured this way
|
||||
onAllow()
|
||||
return
|
||||
return null
|
||||
}
|
||||
AlertDialog.Builder(context).apply {
|
||||
return AlertDialog.Builder(context).apply {
|
||||
setMessage(context.getDisableMessage(appLabel))
|
||||
setPositiveButton(R.string.allow) { _, _ -> onAllow() }
|
||||
setNegativeButton(R.string.deny) { _, _ -> onDeny() }
|
||||
setOnCancelListener { onDeny() }
|
||||
}.show()
|
||||
}.create()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -165,8 +165,7 @@ public class StylusDeviceUpdater implements InputManager.InputDeviceListener,
|
||||
}
|
||||
mUsiPreference.setKey(PREF_KEY);
|
||||
mUsiPreference.setTitle(R.string.stylus_connected_devices_title);
|
||||
// TODO(b/250909304): pending actual icon visD
|
||||
mUsiPreference.setIcon(R.drawable.ic_edit);
|
||||
mUsiPreference.setIcon(R.drawable.ic_stylus);
|
||||
mUsiPreference.setOnPreferenceClickListener((Preference p) -> {
|
||||
mMetricsFeatureProvider.logClickedPreference(p, mFragment.getMetricsCategory());
|
||||
launchDeviceDetails();
|
||||
|
@@ -70,8 +70,7 @@ public class StylusUsiHeaderController extends BasePreferenceController implemen
|
||||
|
||||
ImageView iconView = mHeaderPreference.findViewById(R.id.entity_header_icon);
|
||||
if (iconView != null) {
|
||||
// TODO(b/250909304): get proper icon once VisD ready
|
||||
iconView.setImageResource(R.drawable.ic_edit);
|
||||
iconView.setImageResource(R.drawable.ic_stylus);
|
||||
iconView.setContentDescription("Icon for stylus");
|
||||
}
|
||||
refresh();
|
||||
|
@@ -30,12 +30,12 @@ public class TrackpadReverseScrollingPreferenceController extends TogglePreferen
|
||||
|
||||
@Override
|
||||
public boolean isChecked() {
|
||||
return InputSettings.useTouchpadNaturalScrolling(mContext);
|
||||
return !InputSettings.useTouchpadNaturalScrolling(mContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setChecked(boolean isChecked) {
|
||||
InputSettings.setTouchpadNaturalScrolling(mContext, isChecked);
|
||||
InputSettings.setTouchpadNaturalScrolling(mContext, !isChecked);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@@ -22,14 +22,34 @@ import android.content.Intent
|
||||
import android.os.RemoteException
|
||||
import android.os.UserHandle
|
||||
import android.util.Log
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.android.settings.spa.app.appinfo.AppInfoSettingsProvider
|
||||
import com.android.settingslib.spa.framework.BrowseActivity
|
||||
import com.android.settingslib.spa.framework.common.SettingsPage
|
||||
import com.android.settingslib.spa.framework.util.SESSION_BROWSE
|
||||
import com.android.settingslib.spa.framework.util.SESSION_EXTERNAL
|
||||
import com.android.settingslib.spa.framework.util.appendSpaParams
|
||||
import com.google.android.setupcompat.util.WizardManagerHelper
|
||||
|
||||
class SpaActivity : BrowseActivity() {
|
||||
override fun isPageEnabled(page: SettingsPage) =
|
||||
super.isPageEnabled(page) && !isSuwAndPageBlocked(page.sppName)
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SpaActivity"
|
||||
|
||||
/** The pages that blocked from SUW. */
|
||||
private val SuwBlockedPages = setOf(AppInfoSettingsProvider.name)
|
||||
|
||||
@VisibleForTesting
|
||||
fun Context.isSuwAndPageBlocked(name: String): Boolean =
|
||||
if (name in SuwBlockedPages && !WizardManagerHelper.isDeviceProvisioned(this)) {
|
||||
Log.w(TAG, "$name blocked before SUW completed.");
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun Context.startSpaActivity(destination: String) {
|
||||
val intent = Intent(this, SpaActivity::class.java)
|
||||
|
@@ -19,12 +19,14 @@ package com.android.settings.spa.app.specialaccess
|
||||
import android.Manifest
|
||||
import android.app.AlarmManager
|
||||
import android.app.compat.CompatChanges
|
||||
import android.app.settings.SettingsEnums
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.os.PowerExemptionManager
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import com.android.settings.R
|
||||
import com.android.settings.overlay.FeatureFactory
|
||||
import com.android.settingslib.spa.framework.compose.stateOf
|
||||
import com.android.settingslib.spaprivileged.model.app.AppRecord
|
||||
import com.android.settingslib.spaprivileged.model.app.IPackageManagers
|
||||
@@ -85,6 +87,17 @@ class AlarmsAndRemindersAppListModel(
|
||||
|
||||
override fun setAllowed(record: AlarmsAndRemindersAppRecord, newAllowed: Boolean) {
|
||||
record.controller.setAllowed(newAllowed)
|
||||
logPermissionChange(newAllowed)
|
||||
}
|
||||
|
||||
private fun logPermissionChange(newAllowed: Boolean) {
|
||||
FeatureFactory.getFactory(context).metricsFeatureProvider.action(
|
||||
SettingsEnums.PAGE_UNKNOWN,
|
||||
SettingsEnums.ACTION_ALARMS_AND_REMINDERS_TOGGLE,
|
||||
SettingsEnums.ALARMS_AND_REMINDERS,
|
||||
"",
|
||||
if (newAllowed) 1 else 0
|
||||
)
|
||||
}
|
||||
|
||||
private fun createRecord(
|
||||
|
@@ -18,9 +18,12 @@ package com.android.settings.spa.app.specialaccess
|
||||
|
||||
import android.Manifest
|
||||
import android.app.AppOpsManager
|
||||
import android.app.settings.SettingsEnums
|
||||
import android.content.Context
|
||||
import com.android.settings.R
|
||||
import com.android.settings.overlay.FeatureFactory
|
||||
import com.android.settingslib.spaprivileged.template.app.AppOpPermissionListModel
|
||||
import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord
|
||||
import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListProvider
|
||||
|
||||
object AllFilesAccessAppListProvider : TogglePermissionAppListProvider {
|
||||
@@ -35,4 +38,17 @@ class AllFilesAccessListModel(context: Context) : AppOpPermissionListModel(conte
|
||||
override val appOp = AppOpsManager.OP_MANAGE_EXTERNAL_STORAGE
|
||||
override val permission = Manifest.permission.MANAGE_EXTERNAL_STORAGE
|
||||
override val setModeByUid = true
|
||||
|
||||
override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) {
|
||||
super.setAllowed(record, newAllowed)
|
||||
logPermissionChange(newAllowed)
|
||||
}
|
||||
|
||||
private fun logPermissionChange(newAllowed: Boolean) {
|
||||
val category = when {
|
||||
newAllowed -> SettingsEnums.APP_SPECIAL_PERMISSION_MANAGE_EXT_STRG_ALLOW
|
||||
else -> SettingsEnums.APP_SPECIAL_PERMISSION_MANAGE_EXT_STRG_DENY
|
||||
}
|
||||
FeatureFactory.getFactory(context).metricsFeatureProvider.action(context, category, "")
|
||||
}
|
||||
}
|
||||
|
@@ -18,9 +18,12 @@ package com.android.settings.spa.app.specialaccess
|
||||
|
||||
import android.Manifest
|
||||
import android.app.AppOpsManager
|
||||
import android.app.settings.SettingsEnums
|
||||
import android.content.Context
|
||||
import com.android.settings.R
|
||||
import com.android.settings.overlay.FeatureFactory
|
||||
import com.android.settingslib.spaprivileged.template.app.AppOpPermissionListModel
|
||||
import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord
|
||||
import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListProvider
|
||||
|
||||
object DisplayOverOtherAppsAppListProvider : TogglePermissionAppListProvider {
|
||||
@@ -34,4 +37,17 @@ class DisplayOverOtherAppsListModel(context: Context) : AppOpPermissionListModel
|
||||
override val footerResId = R.string.allow_overlay_description
|
||||
override val appOp = AppOpsManager.OP_SYSTEM_ALERT_WINDOW
|
||||
override val permission = Manifest.permission.SYSTEM_ALERT_WINDOW
|
||||
|
||||
override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) {
|
||||
super.setAllowed(record, newAllowed)
|
||||
logPermissionChange(newAllowed)
|
||||
}
|
||||
|
||||
private fun logPermissionChange(newAllowed: Boolean) {
|
||||
val category = when {
|
||||
newAllowed -> SettingsEnums.APP_SPECIAL_PERMISSION_APPDRAW_ALLOW
|
||||
else -> SettingsEnums.APP_SPECIAL_PERMISSION_APPDRAW_DENY
|
||||
}
|
||||
FeatureFactory.getFactory(context).metricsFeatureProvider.action(context, category, "")
|
||||
}
|
||||
}
|
||||
|
@@ -18,9 +18,12 @@ package com.android.settings.spa.app.specialaccess
|
||||
|
||||
import android.Manifest
|
||||
import android.app.AppOpsManager
|
||||
import android.app.settings.SettingsEnums
|
||||
import android.content.Context
|
||||
import com.android.settings.R
|
||||
import com.android.settings.overlay.FeatureFactory
|
||||
import com.android.settingslib.spaprivileged.template.app.AppOpPermissionListModel
|
||||
import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord
|
||||
import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListProvider
|
||||
|
||||
object MediaManagementAppsAppListProvider : TogglePermissionAppListProvider {
|
||||
@@ -35,4 +38,19 @@ class MediaManagementAppsListModel(context: Context) : AppOpPermissionListModel(
|
||||
override val appOp = AppOpsManager.OP_MANAGE_MEDIA
|
||||
override val permission = Manifest.permission.MANAGE_MEDIA
|
||||
override val setModeByUid = true
|
||||
|
||||
override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) {
|
||||
super.setAllowed(record, newAllowed)
|
||||
logPermissionChange(newAllowed)
|
||||
}
|
||||
|
||||
private fun logPermissionChange(newAllowed: Boolean) {
|
||||
FeatureFactory.getFactory(context).metricsFeatureProvider.action(
|
||||
SettingsEnums.PAGE_UNKNOWN,
|
||||
SettingsEnums.ACTION_MEDIA_MANAGEMENT_APPS_TOGGLE,
|
||||
SettingsEnums.MEDIA_MANAGEMENT_APPS,
|
||||
"",
|
||||
if (newAllowed) 1 else 0
|
||||
)
|
||||
}
|
||||
}
|
@@ -18,9 +18,12 @@ package com.android.settings.spa.app.specialaccess
|
||||
|
||||
import android.Manifest
|
||||
import android.app.AppOpsManager
|
||||
import android.app.settings.SettingsEnums
|
||||
import android.content.Context
|
||||
import com.android.settings.R
|
||||
import com.android.settings.overlay.FeatureFactory
|
||||
import com.android.settingslib.spaprivileged.template.app.AppOpPermissionListModel
|
||||
import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord
|
||||
import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListProvider
|
||||
|
||||
object ModifySystemSettingsAppListProvider : TogglePermissionAppListProvider {
|
||||
@@ -34,4 +37,17 @@ class ModifySystemSettingsListModel(context: Context) : AppOpPermissionListModel
|
||||
override val footerResId = R.string.write_settings_description
|
||||
override val appOp = AppOpsManager.OP_WRITE_SETTINGS
|
||||
override val permission = Manifest.permission.WRITE_SETTINGS
|
||||
|
||||
override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) {
|
||||
super.setAllowed(record, newAllowed)
|
||||
logPermissionChange(newAllowed)
|
||||
}
|
||||
|
||||
private fun logPermissionChange(newAllowed: Boolean) {
|
||||
val category = when {
|
||||
newAllowed -> SettingsEnums.APP_SPECIAL_PERMISSION_SETTINGS_CHANGE_ALLOW
|
||||
else -> SettingsEnums.APP_SPECIAL_PERMISSION_SETTINGS_CHANGE_DENY
|
||||
}
|
||||
FeatureFactory.getFactory(context).metricsFeatureProvider.action(context, category, "")
|
||||
}
|
||||
}
|
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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.bluetooth
|
||||
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.content.Intent
|
||||
import com.android.settings.testutils.shadow.ShadowAlertDialogCompat
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.android.controller.ActivityController
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.shadow.api.Shadow
|
||||
import org.robolectric.shadows.ShadowBluetoothAdapter
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(shadows = [ShadowAlertDialogCompat::class, ShadowBluetoothAdapter::class])
|
||||
class RequestPermissionActivityTest {
|
||||
private lateinit var activityController: ActivityController<RequestPermissionActivity>
|
||||
private lateinit var bluetoothAdapter: ShadowBluetoothAdapter
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
bluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter())
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
activityController.pause().stop().destroy()
|
||||
ShadowAlertDialogCompat.reset()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun requestEnable_whenBluetoothIsOff_showConfirmDialog() {
|
||||
bluetoothAdapter.setState(BluetoothAdapter.STATE_OFF)
|
||||
|
||||
createActivity(action = BluetoothAdapter.ACTION_REQUEST_ENABLE)
|
||||
|
||||
val dialog = ShadowAlertDialogCompat.getLatestAlertDialog()
|
||||
val shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog)
|
||||
assertThat(shadowDialog.message.toString())
|
||||
.isEqualTo("An app wants to turn on Bluetooth")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun requestEnable_whenBluetoothIsOn_doNothing() {
|
||||
bluetoothAdapter.setState(BluetoothAdapter.STATE_ON)
|
||||
|
||||
createActivity(action = BluetoothAdapter.ACTION_REQUEST_ENABLE)
|
||||
|
||||
val dialog = ShadowAlertDialogCompat.getLatestAlertDialog()
|
||||
assertThat(dialog).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun requestDisable_whenBluetoothIsOff_doNothing() {
|
||||
bluetoothAdapter.setState(BluetoothAdapter.STATE_OFF)
|
||||
|
||||
createActivity(action = BluetoothAdapter.ACTION_REQUEST_DISABLE)
|
||||
|
||||
val dialog = ShadowAlertDialogCompat.getLatestAlertDialog()
|
||||
assertThat(dialog).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun requestDisable_whenBluetoothIsOn_showConfirmDialog() {
|
||||
bluetoothAdapter.setState(BluetoothAdapter.STATE_ON)
|
||||
|
||||
createActivity(action = BluetoothAdapter.ACTION_REQUEST_DISABLE)
|
||||
|
||||
val dialog = ShadowAlertDialogCompat.getLatestAlertDialog()
|
||||
val shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog)
|
||||
assertThat(shadowDialog.message.toString())
|
||||
.isEqualTo("An app wants to turn off Bluetooth")
|
||||
}
|
||||
|
||||
private fun createActivity(action: String) {
|
||||
activityController =
|
||||
ActivityController.of(RequestPermissionActivity(), Intent(action)).apply {
|
||||
create()
|
||||
start()
|
||||
postCreate(null)
|
||||
resume()
|
||||
}
|
||||
}
|
||||
}
|
@@ -49,13 +49,14 @@ class RequestPermissionHelperTest {
|
||||
@Test
|
||||
fun requestEnable_withAppLabelAndNoTimeout_hasCorrectMessage() {
|
||||
val activity = activityController.get()
|
||||
|
||||
RequestPermissionHelper.requestEnable(
|
||||
context = activity,
|
||||
appLabel = "App Label",
|
||||
timeout = -1,
|
||||
onAllow = {},
|
||||
onDeny = {},
|
||||
)
|
||||
)!!.show()
|
||||
|
||||
assertLatestMessageIs("App Label wants to turn on Bluetooth")
|
||||
}
|
||||
@@ -63,13 +64,14 @@ class RequestPermissionHelperTest {
|
||||
@Test
|
||||
fun requestEnable_withAppLabelAndZeroTimeout_hasCorrectMessage() {
|
||||
val activity = activityController.get()
|
||||
|
||||
RequestPermissionHelper.requestEnable(
|
||||
context = activity,
|
||||
appLabel = "App Label",
|
||||
timeout = 0,
|
||||
onAllow = {},
|
||||
onDeny = {},
|
||||
)
|
||||
)!!.show()
|
||||
|
||||
assertLatestMessageIs(
|
||||
"App Label wants to turn on Bluetooth and make your phone visible to other devices. " +
|
||||
@@ -80,13 +82,14 @@ class RequestPermissionHelperTest {
|
||||
@Test
|
||||
fun requestEnable_withAppLabelAndNormalTimeout_hasCorrectMessage() {
|
||||
val activity = activityController.get()
|
||||
|
||||
RequestPermissionHelper.requestEnable(
|
||||
context = activity,
|
||||
appLabel = "App Label",
|
||||
timeout = 120,
|
||||
onAllow = {},
|
||||
onDeny = {},
|
||||
)
|
||||
)!!.show()
|
||||
|
||||
assertLatestMessageIs(
|
||||
"App Label wants to turn on Bluetooth and make your phone visible to other devices " +
|
||||
@@ -97,13 +100,14 @@ class RequestPermissionHelperTest {
|
||||
@Test
|
||||
fun requestEnable_withNoAppLabelAndNoTimeout_hasCorrectMessage() {
|
||||
val activity = activityController.get()
|
||||
|
||||
RequestPermissionHelper.requestEnable(
|
||||
context = activity,
|
||||
appLabel = null,
|
||||
timeout = -1,
|
||||
onAllow = {},
|
||||
onDeny = {},
|
||||
)
|
||||
)!!.show()
|
||||
|
||||
assertLatestMessageIs("An app wants to turn on Bluetooth")
|
||||
}
|
||||
@@ -111,13 +115,14 @@ class RequestPermissionHelperTest {
|
||||
@Test
|
||||
fun requestEnable_withNoAppLabelAndZeroTimeout_hasCorrectMessage() {
|
||||
val activity = activityController.get()
|
||||
|
||||
RequestPermissionHelper.requestEnable(
|
||||
context = activity,
|
||||
appLabel = null,
|
||||
timeout = 0,
|
||||
onAllow = {},
|
||||
onDeny = {},
|
||||
)
|
||||
)!!.show()
|
||||
|
||||
assertLatestMessageIs(
|
||||
"An app wants to turn on Bluetooth and make your phone visible to other devices. " +
|
||||
@@ -128,13 +133,14 @@ class RequestPermissionHelperTest {
|
||||
@Test
|
||||
fun requestEnable_withNoAppLabelAndNormalTimeout_hasCorrectMessage() {
|
||||
val activity = activityController.get()
|
||||
|
||||
RequestPermissionHelper.requestEnable(
|
||||
context = activity,
|
||||
appLabel = null,
|
||||
timeout = 120,
|
||||
onAllow = {},
|
||||
onDeny = {},
|
||||
)
|
||||
)!!.show()
|
||||
|
||||
assertLatestMessageIs(
|
||||
"An app wants to turn on Bluetooth and make your phone visible to other devices for " +
|
||||
@@ -177,12 +183,13 @@ class RequestPermissionHelperTest {
|
||||
@Test
|
||||
fun requestDisable_withAppLabel_hasCorrectMessage() {
|
||||
val activity = activityController.get()
|
||||
|
||||
RequestPermissionHelper.requestDisable(
|
||||
context = activity,
|
||||
appLabel = "App Label",
|
||||
onAllow = {},
|
||||
onDeny = {},
|
||||
)
|
||||
)!!.show()
|
||||
|
||||
assertLatestMessageIs("App Label wants to turn off Bluetooth")
|
||||
}
|
||||
@@ -190,12 +197,13 @@ class RequestPermissionHelperTest {
|
||||
@Test
|
||||
fun requestDisable_withNoAppLabel_hasCorrectMessage() {
|
||||
val activity = activityController.get()
|
||||
|
||||
RequestPermissionHelper.requestDisable(
|
||||
context = activity,
|
||||
appLabel = null,
|
||||
onAllow = {},
|
||||
onDeny = {},
|
||||
)
|
||||
)!!.show()
|
||||
|
||||
assertLatestMessageIs("An app wants to turn off Bluetooth")
|
||||
}
|
||||
|
@@ -61,22 +61,9 @@ public class TrackpadReverseScrollingPreferenceControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setChecked_true_shouldReturn1() {
|
||||
public void setChecked_true_shouldReturn0() {
|
||||
mController.setChecked(true);
|
||||
|
||||
int result = Settings.System.getIntForUser(
|
||||
mContext.getContentResolver(),
|
||||
SETTING_KEY,
|
||||
0,
|
||||
UserHandle.USER_CURRENT);
|
||||
|
||||
assertThat(result).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setChecked_false_shouldReturn0() {
|
||||
mController.setChecked(false);
|
||||
|
||||
int result = Settings.System.getIntForUser(
|
||||
mContext.getContentResolver(),
|
||||
SETTING_KEY,
|
||||
@@ -87,7 +74,20 @@ public class TrackpadReverseScrollingPreferenceControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isChecked_providerPutInt1_returnTrue() {
|
||||
public void setChecked_false_shouldReturn1() {
|
||||
mController.setChecked(false);
|
||||
|
||||
int result = Settings.System.getIntForUser(
|
||||
mContext.getContentResolver(),
|
||||
SETTING_KEY,
|
||||
0,
|
||||
UserHandle.USER_CURRENT);
|
||||
|
||||
assertThat(result).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isChecked_providerPutInt1_returnFalse() {
|
||||
Settings.System.putIntForUser(
|
||||
mContext.getContentResolver(),
|
||||
SETTING_KEY,
|
||||
@@ -96,11 +96,11 @@ public class TrackpadReverseScrollingPreferenceControllerTest {
|
||||
|
||||
boolean result = mController.isChecked();
|
||||
|
||||
assertThat(result).isTrue();
|
||||
assertThat(result).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isChecked_providerPutInt0_returnFalse() {
|
||||
public void isChecked_providerPutInt0_returnTrue() {
|
||||
Settings.System.putIntForUser(
|
||||
mContext.getContentResolver(),
|
||||
SETTING_KEY,
|
||||
@@ -109,6 +109,6 @@ public class TrackpadReverseScrollingPreferenceControllerTest {
|
||||
|
||||
boolean result = mController.isChecked();
|
||||
|
||||
assertThat(result).isFalse();
|
||||
assertThat(result).isTrue();
|
||||
}
|
||||
}
|
||||
|
@@ -21,33 +21,71 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.UserHandle
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.android.dx.mockito.inline.extended.ExtendedMockito
|
||||
import com.android.settings.spa.SpaActivity.Companion.isSuwAndPageBlocked
|
||||
import com.android.settings.spa.SpaActivity.Companion.startSpaActivity
|
||||
import com.android.settings.spa.SpaActivity.Companion.startSpaActivityForApp
|
||||
import com.android.settings.spa.app.AllAppListPageProvider
|
||||
import com.android.settings.spa.app.appinfo.AppInfoSettingsProvider
|
||||
import com.android.settingslib.spa.framework.util.KEY_DESTINATION
|
||||
import com.google.android.setupcompat.util.WizardManagerHelper
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Answers
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.junit.MockitoJUnit
|
||||
import org.mockito.junit.MockitoRule
|
||||
import org.mockito.MockitoSession
|
||||
import org.mockito.quality.Strictness
|
||||
import org.mockito.Mockito.`when` as whenever
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SpaActivityTest {
|
||||
@get:Rule
|
||||
val mockito: MockitoRule = MockitoJUnit.rule()
|
||||
private lateinit var mockSession: MockitoSession
|
||||
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
@Mock
|
||||
private lateinit var context: Context
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
`when`(context.applicationContext.packageName).thenReturn("com.android.settings")
|
||||
mockSession = ExtendedMockito.mockitoSession()
|
||||
.initMocks(this)
|
||||
.mockStatic(WizardManagerHelper::class.java)
|
||||
.strictness(Strictness.LENIENT)
|
||||
.startMocking()
|
||||
whenever(context.applicationContext).thenReturn(context)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
mockSession.finishMocking()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isSuwAndPageBlocked_regularPage_notBlocked() {
|
||||
val isBlocked = context.isSuwAndPageBlocked(AllAppListPageProvider.name)
|
||||
|
||||
assertThat(isBlocked).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isSuwAndPageBlocked_blocklistedPageInSuw_blocked() {
|
||||
whenever(WizardManagerHelper.isDeviceProvisioned(context)).thenReturn(false)
|
||||
|
||||
val isBlocked = context.isSuwAndPageBlocked(AppInfoSettingsProvider.name)
|
||||
|
||||
assertThat(isBlocked).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isSuwAndPageBlocked_blocklistedPageNotInSuw_notBlocked() {
|
||||
whenever(WizardManagerHelper.isDeviceProvisioned(context)).thenReturn(true)
|
||||
|
||||
val isBlocked = context.isSuwAndPageBlocked(AppInfoSettingsProvider.name)
|
||||
|
||||
assertThat(isBlocked).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
Reference in New Issue
Block a user