Add high power whitelist for apps
- Strings not final! - New UX for power usage details (more preferency) - Add high power apps list shows on/off and screen to change (when possible) - Link from power usage summary to high power list - Link from advanced apps to high power list Bug: 19991702 Change-Id: I97c927ed82d3b89041e4429b427508545763d66c
This commit is contained in:
@@ -26,6 +26,7 @@ import com.android.settings.R;
|
||||
import com.android.settings.SettingsPreferenceFragment;
|
||||
import com.android.settings.applications.ApplicationsState.AppEntry;
|
||||
import com.android.settings.applications.ApplicationsState.Session;
|
||||
import com.android.settings.fuelgauge.PowerWhitelistBackend;
|
||||
import com.android.settingslib.applications.PermissionsInfo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -38,11 +39,13 @@ public class AdvancedAppSettings extends SettingsPreferenceFragment implements
|
||||
|
||||
private static final String KEY_APP_PERM = "manage_perms";
|
||||
private static final String KEY_APP_DOMAIN_URLS = "domain_urls";
|
||||
private static final String KEY_HIGH_POWER_APPS = "high_power_apps";
|
||||
|
||||
private ApplicationsState mApplicationsState;
|
||||
private Session mSession;
|
||||
private Preference mAppPermsPreference;
|
||||
private Preference mAppDomainURLsPreference;
|
||||
private Preference mHighPowerPreference;
|
||||
private PermissionsInfo mPermissionsInfo;
|
||||
|
||||
@Override
|
||||
@@ -55,6 +58,7 @@ public class AdvancedAppSettings extends SettingsPreferenceFragment implements
|
||||
|
||||
mAppPermsPreference = findPreference(KEY_APP_PERM);
|
||||
mAppDomainURLsPreference = findPreference(KEY_APP_DOMAIN_URLS);
|
||||
mHighPowerPreference = findPreference(KEY_HIGH_POWER_APPS);
|
||||
updateUI();
|
||||
}
|
||||
|
||||
@@ -70,6 +74,10 @@ public class AdvancedAppSettings extends SettingsPreferenceFragment implements
|
||||
String summary = getResources().getQuantityString(
|
||||
R.plurals.domain_urls_apps_summary, countAppWithDomainURLs, countAppWithDomainURLs);
|
||||
mAppDomainURLsPreference.setSummary(summary);
|
||||
|
||||
int highPowerCount = PowerWhitelistBackend.getInstance().getWhitelistSize();
|
||||
mHighPowerPreference.setSummary(getResources().getQuantityString(R.plurals.high_power_count,
|
||||
highPowerCount, highPowerCount));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -20,6 +20,7 @@ import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.app.DialogFragment;
|
||||
import android.app.Fragment;
|
||||
import android.app.admin.DevicePolicyManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -36,6 +37,7 @@ import android.util.Log;
|
||||
|
||||
import com.android.settings.InstrumentedPreferenceFragment;
|
||||
import com.android.settings.SettingsActivity;
|
||||
import com.android.settings.Utils;
|
||||
import com.android.settings.applications.ApplicationsState.AppEntry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -194,6 +196,17 @@ public abstract class AppInfoBase extends InstrumentedPreferenceFragment
|
||||
refreshUi();
|
||||
}
|
||||
|
||||
public static void startAppInfoFragment(Class<? extends AppInfoBase> fragment, int titleRes,
|
||||
String pkg, int uid, Fragment source, int request) {
|
||||
Bundle args = new Bundle();
|
||||
args.putString(AppInfoBase.ARG_PACKAGE_NAME, pkg);
|
||||
|
||||
Intent intent = Utils.onBuildStartFragmentIntent(source.getActivity(), fragment.getName(),
|
||||
args, null, titleRes, null, false);
|
||||
source.getActivity().startActivityForResultAsUser(intent, request,
|
||||
new UserHandle(UserHandle.getUserId(uid)));
|
||||
}
|
||||
|
||||
public class MyAlertDialogFragment extends DialogFragment {
|
||||
public MyAlertDialogFragment(int id, int errorCode) {
|
||||
Bundle args = new Bundle();
|
||||
|
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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.applications;
|
||||
|
||||
import com.android.settings.applications.ApplicationsState.AppEntry;
|
||||
import com.android.settings.applications.ApplicationsState.AppFilter;
|
||||
import com.android.settings.fuelgauge.PowerWhitelistBackend;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Connects data from the PowerWhitelistBackend to ApplicationsState.
|
||||
*/
|
||||
public class AppStatePowerBridge extends AppStateBaseBridge {
|
||||
|
||||
private final PowerWhitelistBackend mBackend = PowerWhitelistBackend.getInstance();
|
||||
|
||||
public AppStatePowerBridge(ApplicationsState appState, Callback callback) {
|
||||
super(appState, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void loadAllExtraInfo() {
|
||||
ArrayList<AppEntry> apps = mAppSession.getAllApps();
|
||||
final int N = apps.size();
|
||||
for (int i = 0; i < N; i++) {
|
||||
AppEntry app = apps.get(i);
|
||||
app.extraInfo = mBackend.isWhitelisted(app.info.packageName)
|
||||
? Boolean.TRUE : Boolean.FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateExtraInfo(AppEntry app, String pkg, int uid) {
|
||||
app.extraInfo = mBackend.isWhitelisted(pkg) ? Boolean.TRUE : Boolean.FALSE;
|
||||
}
|
||||
|
||||
public static class HighPowerState {
|
||||
public boolean isHighPower;
|
||||
public boolean isSystemHighPower;
|
||||
}
|
||||
|
||||
public static final AppFilter FILTER_POWER_WHITELISTED = new AppFilter() {
|
||||
@Override
|
||||
public void init() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean filterApp(AppEntry info) {
|
||||
return info.extraInfo == Boolean.TRUE;
|
||||
}
|
||||
};
|
||||
|
||||
public static final AppFilter FILTER_POWER_NOT_WHITELISTED = new AppFilter() {
|
||||
@Override
|
||||
public void init() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean filterApp(AppEntry info) {
|
||||
return info.extraInfo == Boolean.FALSE;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
@@ -38,6 +38,7 @@ import android.net.NetworkTemplate;
|
||||
import android.net.TrafficStats;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.BatteryStats;
|
||||
import android.os.Bundle;
|
||||
import android.os.RemoteException;
|
||||
import android.os.ServiceManager;
|
||||
@@ -57,12 +58,16 @@ import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.android.internal.logging.MetricsLogger;
|
||||
import com.android.internal.os.BatterySipper;
|
||||
import com.android.internal.os.BatteryStatsHelper;
|
||||
import com.android.settings.DataUsageSummary;
|
||||
import com.android.settings.DataUsageSummary.AppItem;
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.SettingsActivity;
|
||||
import com.android.settings.Utils;
|
||||
import com.android.settings.applications.ApplicationsState.AppEntry;
|
||||
import com.android.settings.fuelgauge.BatteryEntry;
|
||||
import com.android.settings.fuelgauge.PowerUsageDetail;
|
||||
import com.android.settings.net.ChartData;
|
||||
import com.android.settings.net.ChartDataLoader;
|
||||
import com.android.settings.notification.NotificationBackend;
|
||||
@@ -108,6 +113,7 @@ public class InstalledAppDetails extends AppInfoBase
|
||||
private static final String KEY_PERMISSION = "permission_settings";
|
||||
private static final String KEY_DATA = "data_settings";
|
||||
private static final String KEY_LAUNCH = "preferred_settings";
|
||||
private static final String KEY_BATTERY = "battery";
|
||||
|
||||
private final HashSet<String> mHomePackages = new HashSet<String>();
|
||||
|
||||
@@ -131,6 +137,11 @@ public class InstalledAppDetails extends AppInfoBase
|
||||
private ChartData mChartData;
|
||||
private INetworkStatsSession mStatsSession;
|
||||
|
||||
private Preference mBatteryPreference;
|
||||
|
||||
private BatteryStatsHelper mBatteryHelper;
|
||||
private BatterySipper mSipper;
|
||||
|
||||
private boolean handleDisableable(Button button) {
|
||||
boolean disableable = false;
|
||||
// Try to prevent the user from bricking their phone
|
||||
@@ -221,6 +232,7 @@ public class InstalledAppDetails extends AppInfoBase
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
mBatteryHelper = new BatteryStatsHelper(getActivity(), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -236,6 +248,7 @@ public class InstalledAppDetails extends AppInfoBase
|
||||
getLoaderManager().restartLoader(LOADER_CHART_DATA,
|
||||
ChartDataLoader.buildArgs(NetworkTemplate.buildTemplateMobileWildcard(), app),
|
||||
mDataCallbacks);
|
||||
new BatteryUpdater().execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -263,6 +276,9 @@ public class InstalledAppDetails extends AppInfoBase
|
||||
mPermissionsPreference.setOnPreferenceClickListener(this);
|
||||
mDataPreference = findPreference(KEY_DATA);
|
||||
mDataPreference.setOnPreferenceClickListener(this);
|
||||
mBatteryPreference = findPreference(KEY_BATTERY);
|
||||
mBatteryPreference.setEnabled(false);
|
||||
mBatteryPreference.setOnPreferenceClickListener(this);
|
||||
|
||||
mLaunchPreference = findPreference(KEY_LAUNCH);
|
||||
if ((mAppEntry.info.flags&ApplicationInfo.FLAG_INSTALLED) == 0) {
|
||||
@@ -434,6 +450,8 @@ public class InstalledAppDetails extends AppInfoBase
|
||||
mBackend));
|
||||
mDataPreference.setSummary(getDataSummary());
|
||||
|
||||
updateBattery();
|
||||
|
||||
if (!mInitialized) {
|
||||
// First time init: are we displaying an uninstalled app?
|
||||
mInitialized = true;
|
||||
@@ -459,6 +477,20 @@ public class InstalledAppDetails extends AppInfoBase
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateBattery() {
|
||||
if (mSipper != null) {
|
||||
mBatteryPreference.setEnabled(true);
|
||||
int dischargeAmount = mBatteryHelper.getStats().getDischargeAmount(
|
||||
BatteryStats.STATS_SINCE_CHARGED);
|
||||
final int percentOfMax = (int) ((mSipper.totalPowerMah)
|
||||
/ mBatteryHelper.getTotalPower() * dischargeAmount + .5f);
|
||||
mBatteryPreference.setSummary(getString(R.string.battery_summary, percentOfMax));
|
||||
} else {
|
||||
mBatteryPreference.setEnabled(false);
|
||||
mBatteryPreference.setSummary(getString(R.string.no_battery_summary));
|
||||
}
|
||||
}
|
||||
|
||||
private CharSequence getDataSummary() {
|
||||
if (mChartData != null) {
|
||||
long totalBytes = mChartData.detail.getTotalBytes();
|
||||
@@ -656,6 +688,10 @@ public class InstalledAppDetails extends AppInfoBase
|
||||
SettingsActivity sa = (SettingsActivity) getActivity();
|
||||
sa.startPreferencePanel(DataUsageSummary.class.getName(), args, -1,
|
||||
getString(R.string.app_data_usage), this, SUB_INFO_FRAGMENT);
|
||||
} else if (preference == mBatteryPreference) {
|
||||
BatteryEntry entry = new BatteryEntry(getActivity(), null, mUserManager, mSipper);
|
||||
PowerUsageDetail.startBatteryDetailPage((SettingsActivity) getActivity(),
|
||||
mBatteryHelper, BatteryStats.STATS_SINCE_CHARGED, entry, true);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -700,7 +736,31 @@ public class InstalledAppDetails extends AppInfoBase
|
||||
}
|
||||
}
|
||||
|
||||
static class DisableChanger extends AsyncTask<Object, Object, Object> {
|
||||
private class BatteryUpdater extends AsyncTask<Void, Void, Void> {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
mBatteryHelper.create((Bundle) null);
|
||||
mBatteryHelper.refreshStats(BatteryStats.STATS_SINCE_CHARGED,
|
||||
mUserManager.getUserProfiles());
|
||||
List<BatterySipper> usageList = mBatteryHelper.getUsageList();
|
||||
final int N = usageList.size();
|
||||
for (int i = 0; i < N; i++) {
|
||||
BatterySipper sipper = usageList.get(i);
|
||||
if (sipper.getUid() == mPackageInfo.applicationInfo.uid) {
|
||||
mSipper = sipper;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
refreshUi();
|
||||
}
|
||||
}
|
||||
|
||||
private static class DisableChanger extends AsyncTask<Object, Object, Object> {
|
||||
final PackageManager mPm;
|
||||
final WeakReference<InstalledAppDetails> mActivity;
|
||||
final ApplicationInfo mInfo;
|
||||
|
@@ -45,7 +45,9 @@ public class LayoutPreference extends Preference {
|
||||
.inflate(layoutResource, null, false);
|
||||
|
||||
final ViewGroup allDetails = (ViewGroup) view.findViewById(R.id.all_details);
|
||||
Utils.forceCustomPadding(allDetails, true /* additive padding */);
|
||||
if (allDetails != null) {
|
||||
Utils.forceCustomPadding(allDetails, true /* additive padding */);
|
||||
}
|
||||
mRootView = view;
|
||||
setShouldDisableView(false);
|
||||
}
|
||||
|
@@ -55,6 +55,7 @@ import com.android.settings.InstrumentedFragment;
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.Settings.AllApplicationsActivity;
|
||||
import com.android.settings.Settings.DomainsURLsAppListActivity;
|
||||
import com.android.settings.Settings.HighPowerApplicationsActivity;
|
||||
import com.android.settings.Settings.NotificationAppListActivity;
|
||||
import com.android.settings.Settings.StorageUseActivity;
|
||||
import com.android.settings.Settings.UsageAccessSettingsActivity;
|
||||
@@ -65,6 +66,7 @@ import com.android.settings.applications.ApplicationsState.AppEntry;
|
||||
import com.android.settings.applications.ApplicationsState.AppFilter;
|
||||
import com.android.settings.applications.ApplicationsState.CompoundFilter;
|
||||
import com.android.settings.applications.ApplicationsState.VolumeFilter;
|
||||
import com.android.settings.fuelgauge.HighPowerDetail;
|
||||
import com.android.settings.notification.NotificationBackend;
|
||||
import com.android.settings.notification.NotificationBackend.AppRow;
|
||||
|
||||
@@ -117,6 +119,8 @@ public class ManageApplications extends InstrumentedFragment
|
||||
public static final int FILTER_APPS_WORK = 8;
|
||||
public static final int FILTER_APPS_WITH_DOMAIN_URLS = 9;
|
||||
public static final int FILTER_APPS_USAGE_ACCESS = 10;
|
||||
public static final int FILTER_APPS_POWER_WHITELIST = 11;
|
||||
public static final int FILTER_APPS_POWER_NO_WHITELIST = 12;
|
||||
|
||||
// This is the string labels for the filter modes above, the order must be kept in sync.
|
||||
public static final int[] FILTER_LABELS = new int[] {
|
||||
@@ -131,6 +135,8 @@ public class ManageApplications extends InstrumentedFragment
|
||||
R.string.filter_work_apps, // Work
|
||||
R.string.filter_with_domain_urls_apps, // Domain URLs
|
||||
R.string.filter_all_apps, // Usage access screen, never displayed
|
||||
R.string.high_power_on, // High power whitelist, on
|
||||
R.string.high_power_off, // High power whitelist, off
|
||||
};
|
||||
// This is the actual mapping to filters from FILTER_ constants above, the order must
|
||||
// be kept in sync.
|
||||
@@ -146,6 +152,8 @@ public class ManageApplications extends InstrumentedFragment
|
||||
ApplicationsState.FILTER_WORK, // Work
|
||||
ApplicationsState.FILTER_WITH_DOMAIN_URLS, // Apps with Domain URLs
|
||||
AppStateUsageBridge.FILTER_APP_USAGE, // Apps with Domain URLs
|
||||
AppStatePowerBridge.FILTER_POWER_WHITELISTED, // High power whitelist, on
|
||||
AppStatePowerBridge.FILTER_POWER_NOT_WHITELISTED, // High power whitelist, off
|
||||
};
|
||||
|
||||
// sort order
|
||||
@@ -180,11 +188,12 @@ public class ManageApplications extends InstrumentedFragment
|
||||
|
||||
private Menu mOptionsMenu;
|
||||
|
||||
public static final int LIST_TYPE_MAIN = 0;
|
||||
public static final int LIST_TYPE_MAIN = 0;
|
||||
public static final int LIST_TYPE_NOTIFICATION = 1;
|
||||
public static final int LIST_TYPE_DOMAINS_URLS = 2;
|
||||
public static final int LIST_TYPE_STORAGE = 3;
|
||||
public static final int LIST_TYPE_STORAGE = 3;
|
||||
public static final int LIST_TYPE_USAGE_ACCESS = 4;
|
||||
public static final int LIST_TYPE_HIGH_POWER = 5;
|
||||
|
||||
private View mRootView;
|
||||
|
||||
@@ -228,6 +237,8 @@ public class ManageApplications extends InstrumentedFragment
|
||||
} else if (className.equals(UsageAccessSettingsActivity.class.getName())) {
|
||||
mListType = LIST_TYPE_USAGE_ACCESS;
|
||||
getActivity().getActionBar().setTitle(R.string.usage_access_title);
|
||||
} else if (className.equals(HighPowerApplicationsActivity.class.getName())) {
|
||||
mListType = LIST_TYPE_HIGH_POWER;
|
||||
} else {
|
||||
mListType = LIST_TYPE_MAIN;
|
||||
}
|
||||
@@ -310,6 +321,9 @@ public class ManageApplications extends InstrumentedFragment
|
||||
mFilterAdapter.enableFilter(FILTER_APPS_SENSITIVE);
|
||||
mFilterAdapter.enableFilter(FILTER_APPS_NO_PEEKING);
|
||||
}
|
||||
if (mListType == LIST_TYPE_HIGH_POWER) {
|
||||
mFilterAdapter.enableFilter(FILTER_APPS_POWER_NO_WHITELIST);
|
||||
}
|
||||
if (mListType == LIST_TYPE_STORAGE) {
|
||||
mApplications.setOverrideFilter(new VolumeFilter(mVolumeUuid));
|
||||
}
|
||||
@@ -325,12 +339,12 @@ public class ManageApplications extends InstrumentedFragment
|
||||
|
||||
private int getDefaultFilter() {
|
||||
switch (mListType) {
|
||||
case LIST_TYPE_MAIN:
|
||||
return FILTER_APPS_ALL;
|
||||
case LIST_TYPE_DOMAINS_URLS:
|
||||
return FILTER_APPS_WITH_DOMAIN_URLS;
|
||||
case LIST_TYPE_USAGE_ACCESS:
|
||||
return FILTER_APPS_USAGE_ACCESS;
|
||||
case LIST_TYPE_HIGH_POWER:
|
||||
return FILTER_APPS_POWER_WHITELIST;
|
||||
default:
|
||||
return FILTER_APPS_ALL;
|
||||
}
|
||||
@@ -349,6 +363,8 @@ public class ManageApplications extends InstrumentedFragment
|
||||
return InstrumentedFragment.VIEW_CATEGORY_STORAGE_APPS;
|
||||
case LIST_TYPE_USAGE_ACCESS:
|
||||
return MetricsLogger.USAGE_ACCESS;
|
||||
case LIST_TYPE_HIGH_POWER:
|
||||
return InstrumentedFragment.VIEW_CATEGORY_HIGH_POWER_APPS;
|
||||
default:
|
||||
return MetricsLogger.VIEW_UNKNOWN;
|
||||
}
|
||||
@@ -426,6 +442,9 @@ public class ManageApplications extends InstrumentedFragment
|
||||
case LIST_TYPE_STORAGE:
|
||||
startAppInfoFragment(AppStorageSettings.class, R.string.storage_settings);
|
||||
break;
|
||||
case LIST_TYPE_HIGH_POWER:
|
||||
startAppInfoFragment(HighPowerDetail.class, R.string.high_power);
|
||||
break;
|
||||
// TODO: Figure out if there is a way where we can spin up the profile's settings
|
||||
// process ahead of time, to avoid a long load of data when user clicks on a managed app.
|
||||
// Maybe when they load the list of apps that contains managed profile apps.
|
||||
@@ -436,13 +455,8 @@ public class ManageApplications extends InstrumentedFragment
|
||||
}
|
||||
|
||||
private void startAppInfoFragment(Class<? extends AppInfoBase> fragment, int titleRes) {
|
||||
Bundle args = new Bundle();
|
||||
args.putString(AppInfoBase.ARG_PACKAGE_NAME, mCurrentPkgName);
|
||||
|
||||
Intent intent = Utils.onBuildStartFragmentIntent(getActivity(), fragment.getName(), args,
|
||||
null, titleRes, null, false);
|
||||
getActivity().startActivityForResultAsUser(intent, INSTALLED_APP_DETAILS,
|
||||
new UserHandle(UserHandle.getUserId(mCurrentUid)));
|
||||
AppInfoBase.startAppInfoFragment(fragment, titleRes, mCurrentPkgName, mCurrentUid, this,
|
||||
INSTALLED_APP_DETAILS);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -685,6 +699,8 @@ public class ManageApplications extends InstrumentedFragment
|
||||
mState, this, manageApplications.mNotifBackend);
|
||||
} else if (mManageApplications.mListType == LIST_TYPE_USAGE_ACCESS) {
|
||||
mExtraInfoBridge = new AppStateUsageBridge(mContext, mState, this);
|
||||
} else if (mManageApplications.mListType == LIST_TYPE_HIGH_POWER) {
|
||||
mExtraInfoBridge = new AppStatePowerBridge(mState, this);
|
||||
} else {
|
||||
mExtraInfoBridge = null;
|
||||
}
|
||||
@@ -994,6 +1010,10 @@ public class ManageApplications extends InstrumentedFragment
|
||||
}
|
||||
break;
|
||||
|
||||
case LIST_TYPE_HIGH_POWER:
|
||||
holder.summary.setText(HighPowerDetail.getSummary(mContext, holder.entry));
|
||||
break;
|
||||
|
||||
default:
|
||||
holder.updateSizeText(mManageApplications.mInvalidSizeStr, mWhichSize);
|
||||
break;
|
||||
|
Reference in New Issue
Block a user