Merge "Change to get network history from NetworkStatsManager."
This commit is contained in:
@@ -24,6 +24,8 @@ import static android.telephony.TelephonyManager.SIM_STATE_READY;
|
||||
import static android.text.format.DateUtils.FORMAT_ABBREV_MONTH;
|
||||
import static android.text.format.DateUtils.FORMAT_SHOW_DATE;
|
||||
|
||||
import android.app.usage.NetworkStats.Bucket;
|
||||
import android.app.usage.NetworkStatsManager;
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.INetworkStatsService;
|
||||
@@ -37,6 +39,7 @@ import android.os.ServiceManager;
|
||||
import android.telephony.SubscriptionManager;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.FeatureFlagUtils;
|
||||
import android.util.Log;
|
||||
import android.util.Range;
|
||||
|
||||
@@ -51,6 +54,8 @@ import java.util.Locale;
|
||||
public class DataUsageController {
|
||||
|
||||
private static final String TAG = "DataUsageController";
|
||||
@VisibleForTesting
|
||||
static final String DATA_USAGE_V2 = "settings_data_usage_v2";
|
||||
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
|
||||
private static final int FIELDS = FIELD_RX_BYTES | FIELD_TX_BYTES;
|
||||
private static final StringBuilder PERIOD_BUILDER = new StringBuilder(50);
|
||||
@@ -62,6 +67,7 @@ public class DataUsageController {
|
||||
private final ConnectivityManager mConnectivityManager;
|
||||
private final INetworkStatsService mStatsService;
|
||||
private final NetworkPolicyManager mPolicyManager;
|
||||
private final NetworkStatsManager mNetworkStatsManager;
|
||||
|
||||
private INetworkStatsSession mSession;
|
||||
private Callback mCallback;
|
||||
@@ -74,6 +80,7 @@ public class DataUsageController {
|
||||
mStatsService = INetworkStatsService.Stub.asInterface(
|
||||
ServiceManager.getService(Context.NETWORK_STATS_SERVICE));
|
||||
mPolicyManager = NetworkPolicyManager.from(mContext);
|
||||
mNetworkStatsManager = context.getSystemService(NetworkStatsManager.class);
|
||||
}
|
||||
|
||||
public void setNetworkController(NetworkNameProvider networkController) {
|
||||
@@ -89,6 +96,7 @@ public class DataUsageController {
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@Deprecated
|
||||
INetworkStatsSession getSession() {
|
||||
if (mSession == null) {
|
||||
try {
|
||||
@@ -128,71 +136,72 @@ public class DataUsageController {
|
||||
}
|
||||
|
||||
public DataUsageInfo getDataUsageInfo(NetworkTemplate template) {
|
||||
final INetworkStatsSession session = getSession();
|
||||
if (session == null) {
|
||||
return warn("no stats session");
|
||||
}
|
||||
final NetworkPolicy policy = findNetworkPolicy(template);
|
||||
try {
|
||||
final NetworkStatsHistory history = session.getHistoryForNetwork(template, FIELDS);
|
||||
final long now = System.currentTimeMillis();
|
||||
final long start, end;
|
||||
final Iterator<Range<ZonedDateTime>> it =
|
||||
(policy != null) ? policy.cycleIterator() : null;
|
||||
if (it != null && it.hasNext()) {
|
||||
final Range<ZonedDateTime> cycle = it.next();
|
||||
start = cycle.getLower().toInstant().toEpochMilli();
|
||||
end = cycle.getUpper().toInstant().toEpochMilli();
|
||||
} else {
|
||||
// period = last 4 wks
|
||||
end = now;
|
||||
start = now - DateUtils.WEEK_IN_MILLIS * 4;
|
||||
}
|
||||
final long callStart = System.currentTimeMillis();
|
||||
final NetworkStatsHistory.Entry entry = history.getValues(start, end, now, null);
|
||||
final long callEnd = System.currentTimeMillis();
|
||||
if (DEBUG) Log.d(TAG, String.format("history call from %s to %s now=%s took %sms: %s",
|
||||
new Date(start), new Date(end), new Date(now), callEnd - callStart,
|
||||
historyEntryToString(entry)));
|
||||
if (entry == null) {
|
||||
return warn("no entry data");
|
||||
}
|
||||
final long totalBytes = entry.rxBytes + entry.txBytes;
|
||||
final DataUsageInfo usage = new DataUsageInfo();
|
||||
usage.startDate = start;
|
||||
usage.usageLevel = totalBytes;
|
||||
usage.period = formatDateRange(start, end);
|
||||
usage.cycleStart = start;
|
||||
usage.cycleEnd = end;
|
||||
|
||||
if (policy != null) {
|
||||
usage.limitLevel = policy.limitBytes > 0 ? policy.limitBytes : 0;
|
||||
usage.warningLevel = policy.warningBytes > 0 ? policy.warningBytes : 0;
|
||||
} else {
|
||||
usage.warningLevel = getDefaultWarningLevel();
|
||||
}
|
||||
if (usage != null && mNetworkController != null) {
|
||||
usage.carrier = mNetworkController.getMobileDataNetworkName();
|
||||
}
|
||||
return usage;
|
||||
} catch (RemoteException e) {
|
||||
return warn("remote call failed");
|
||||
final long now = System.currentTimeMillis();
|
||||
final long start, end;
|
||||
final Iterator<Range<ZonedDateTime>> it = (policy != null) ? policy.cycleIterator() : null;
|
||||
if (it != null && it.hasNext()) {
|
||||
final Range<ZonedDateTime> cycle = it.next();
|
||||
start = cycle.getLower().toInstant().toEpochMilli();
|
||||
end = cycle.getUpper().toInstant().toEpochMilli();
|
||||
} else {
|
||||
// period = last 4 wks
|
||||
end = now;
|
||||
start = now - DateUtils.WEEK_IN_MILLIS * 4;
|
||||
}
|
||||
final long totalBytes;
|
||||
final long callStart = System.currentTimeMillis();
|
||||
if (FeatureFlagUtils.isEnabled(mContext, DATA_USAGE_V2)) {
|
||||
totalBytes = getUsageLevel(template, start, end);
|
||||
} else {
|
||||
totalBytes = getUsageLevel(template, start, end, now);
|
||||
}
|
||||
if (totalBytes < 0L) {
|
||||
return warn("no entry data");
|
||||
}
|
||||
final DataUsageInfo usage = new DataUsageInfo();
|
||||
usage.startDate = start;
|
||||
usage.usageLevel = totalBytes;
|
||||
usage.period = formatDateRange(start, end);
|
||||
usage.cycleStart = start;
|
||||
usage.cycleEnd = end;
|
||||
|
||||
if (policy != null) {
|
||||
usage.limitLevel = policy.limitBytes > 0 ? policy.limitBytes : 0;
|
||||
usage.warningLevel = policy.warningBytes > 0 ? policy.warningBytes : 0;
|
||||
} else {
|
||||
usage.warningLevel = getDefaultWarningLevel();
|
||||
}
|
||||
if (usage != null && mNetworkController != null) {
|
||||
usage.carrier = mNetworkController.getMobileDataNetworkName();
|
||||
}
|
||||
return usage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total usage level recorded in the network history
|
||||
* @param template the network template to retrieve the network history
|
||||
* @return the total usage level recorded in the network history
|
||||
* @return the total usage level recorded in the network history or -1L if there is error
|
||||
* retrieving the data.
|
||||
*/
|
||||
public long getHistoriclUsageLevel(NetworkTemplate template) {
|
||||
public long getHistoricalUsageLevel(NetworkTemplate template) {
|
||||
if (FeatureFlagUtils.isEnabled(mContext, DATA_USAGE_V2)) {
|
||||
return getUsageLevel(template, 0L /* start */, System.currentTimeMillis() /* end */);
|
||||
} else {
|
||||
final long now = System.currentTimeMillis();
|
||||
return getUsageLevel(template, 0L /* start */, now /* end */, now);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
private long getUsageLevel(NetworkTemplate template, long start, long end, long now) {
|
||||
final INetworkStatsSession session = getSession();
|
||||
if (session != null) {
|
||||
try {
|
||||
final NetworkStatsHistory history = session.getHistoryForNetwork(template, FIELDS);
|
||||
final long now = System.currentTimeMillis();
|
||||
final NetworkStatsHistory.Entry entry =
|
||||
history.getValues(0L /* start */, now /* end */, now, null /* recycle */);
|
||||
final NetworkStatsHistory history =
|
||||
session.getHistoryForNetwork(template, FIELDS);
|
||||
final NetworkStatsHistory.Entry entry = history.getValues(
|
||||
start, end, System.currentTimeMillis() /* now */, null /* recycle */);
|
||||
if (entry != null) {
|
||||
return entry.rxBytes + entry.txBytes;
|
||||
}
|
||||
@@ -201,7 +210,21 @@ public class DataUsageController {
|
||||
Log.w(TAG, "Failed to get data usage, remote call failed");
|
||||
}
|
||||
}
|
||||
return 0L;
|
||||
return -1L;
|
||||
}
|
||||
|
||||
private long getUsageLevel(NetworkTemplate template, long start, long end) {
|
||||
try {
|
||||
final Bucket bucket = mNetworkStatsManager.querySummaryForDevice(
|
||||
getNetworkType(template), getActiveSubscriberId(mContext), start, end);
|
||||
if (bucket != null) {
|
||||
return bucket.getRxBytes() + bucket.getTxBytes();
|
||||
}
|
||||
Log.w(TAG, "Failed to get data usage, no entry data");
|
||||
} catch (RemoteException e) {
|
||||
Log.w(TAG, "Failed to get data usage, remote call failed");
|
||||
}
|
||||
return -1L;
|
||||
}
|
||||
|
||||
private NetworkPolicy findNetworkPolicy(NetworkTemplate template) {
|
||||
@@ -218,6 +241,7 @@ public class DataUsageController {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
private static String historyEntryToString(NetworkStatsHistory.Entry entry) {
|
||||
return entry == null ? null : new StringBuilder("Entry[")
|
||||
.append("bucketDuration=").append(entry.bucketDuration)
|
||||
@@ -231,6 +255,17 @@ public class DataUsageController {
|
||||
.append(']').toString();
|
||||
}
|
||||
|
||||
private static String statsBucketToString(Bucket bucket) {
|
||||
return bucket == null ? null : new StringBuilder("Entry[")
|
||||
.append("bucketDuration=").append(bucket.getEndTimeStamp() - bucket.getStartTimeStamp())
|
||||
.append(",bucketStart=").append(bucket.getStartTimeStamp())
|
||||
.append(",rxBytes=").append(bucket.getRxBytes())
|
||||
.append(",rxPackets=").append(bucket.getRxPackets())
|
||||
.append(",txBytes=").append(bucket.getTxBytes())
|
||||
.append(",txPackets=").append(bucket.getTxPackets())
|
||||
.append(']').toString();
|
||||
}
|
||||
|
||||
public void setMobileDataEnabled(boolean enabled) {
|
||||
Log.d(TAG, "setMobileDataEnabled: enabled=" + enabled);
|
||||
mTelephonyManager.setDataEnabled(enabled);
|
||||
@@ -249,6 +284,25 @@ public class DataUsageController {
|
||||
return mTelephonyManager.getDataEnabled();
|
||||
}
|
||||
|
||||
static int getNetworkType(NetworkTemplate networkTemplate) {
|
||||
if (networkTemplate == null) {
|
||||
return ConnectivityManager.TYPE_NONE;
|
||||
}
|
||||
final int matchRule = networkTemplate.getMatchRule();
|
||||
switch (matchRule) {
|
||||
case NetworkTemplate.MATCH_MOBILE:
|
||||
case NetworkTemplate.MATCH_MOBILE_WILDCARD:
|
||||
return ConnectivityManager.TYPE_MOBILE;
|
||||
case NetworkTemplate.MATCH_WIFI:
|
||||
case NetworkTemplate.MATCH_WIFI_WILDCARD:
|
||||
return ConnectivityManager.TYPE_WIFI;
|
||||
case NetworkTemplate.MATCH_ETHERNET:
|
||||
return ConnectivityManager.TYPE_ETHERNET;
|
||||
default:
|
||||
return ConnectivityManager.TYPE_MOBILE;
|
||||
}
|
||||
}
|
||||
|
||||
private static String getActiveSubscriberId(Context context) {
|
||||
final TelephonyManager tele = TelephonyManager.from(context);
|
||||
final String actualSubscriberId = tele.getSubscriberId(
|
||||
|
||||
@@ -43,9 +43,9 @@ public class NetworkCycleChartDataLoader
|
||||
@Override
|
||||
void recordUsage(long start, long end) {
|
||||
try {
|
||||
final NetworkStats stats = mNetworkStatsManager.querySummary(
|
||||
final NetworkStats.Bucket bucket = mNetworkStatsManager.querySummaryForDevice(
|
||||
mNetworkType, mSubId, start, end);
|
||||
final long total = getTotalUsage(stats);
|
||||
final long total = bucket == null ? 0L : bucket.getRxBytes() + bucket.getTxBytes();
|
||||
if (total > 0L) {
|
||||
final NetworkCycleChartData.Builder builder = new NetworkCycleChartData.Builder();
|
||||
builder.setUsageBuckets(getUsageBuckets(start, end))
|
||||
@@ -80,9 +80,11 @@ public class NetworkCycleChartDataLoader
|
||||
while (bucketEnd <= end) {
|
||||
long usage = 0L;
|
||||
try {
|
||||
final NetworkStats stats = mNetworkStatsManager.querySummary(
|
||||
final NetworkStats.Bucket bucket = mNetworkStatsManager.querySummaryForDevice(
|
||||
mNetworkType, mSubId, bucketStart, bucketEnd);
|
||||
usage = getTotalUsage(stats);
|
||||
if (bucket != null) {
|
||||
usage = bucket.getRxBytes() + bucket.getTxBytes();
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Exception querying network detail.", e);
|
||||
}
|
||||
|
||||
@@ -176,31 +176,11 @@ public abstract class NetworkCycleDataLoader<D> extends AsyncTaskLoader<D> {
|
||||
|
||||
public Builder<T> setNetworkTemplate(NetworkTemplate template) {
|
||||
mNetworkTemplate = template;
|
||||
setNetworkType();
|
||||
mNetworkType = DataUsageController.getNetworkType(template);
|
||||
return this;
|
||||
}
|
||||
|
||||
public abstract T build();
|
||||
|
||||
private void setNetworkType() {
|
||||
if (mNetworkTemplate != null) {
|
||||
final int matchRule = mNetworkTemplate.getMatchRule();
|
||||
switch (matchRule) {
|
||||
case NetworkTemplate.MATCH_MOBILE:
|
||||
case NetworkTemplate.MATCH_MOBILE_WILDCARD:
|
||||
mNetworkType = ConnectivityManager.TYPE_MOBILE;
|
||||
break;
|
||||
case NetworkTemplate.MATCH_WIFI:
|
||||
mNetworkType = ConnectivityManager.TYPE_WIFI;
|
||||
break;
|
||||
case NetworkTemplate.MATCH_ETHERNET:
|
||||
mNetworkType = ConnectivityManager.TYPE_ETHERNET;
|
||||
break;
|
||||
default:
|
||||
mNetworkType = ConnectivityManager.TYPE_MOBILE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,16 +24,23 @@ import static org.mockito.Mockito.anyInt;
|
||||
import static org.mockito.Mockito.anyLong;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.app.usage.NetworkStats;
|
||||
import android.app.usage.NetworkStatsManager;
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.INetworkStatsSession;
|
||||
import android.net.NetworkStatsHistory;
|
||||
import android.net.NetworkStatsHistory.Entry;
|
||||
import android.net.NetworkTemplate;
|
||||
import android.os.RemoteException;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.FeatureFlagUtils;
|
||||
|
||||
import com.android.settingslib.SettingsLibRobolectricTestRunner;
|
||||
|
||||
@@ -47,8 +54,14 @@ import org.robolectric.RuntimeEnvironment;
|
||||
@RunWith(SettingsLibRobolectricTestRunner.class)
|
||||
public class DataUsageControllerTest {
|
||||
|
||||
private static final String SUB_ID = "Test Subscriber";
|
||||
|
||||
@Mock
|
||||
private INetworkStatsSession mSession;
|
||||
@Mock
|
||||
private TelephonyManager mTelephonyManager;
|
||||
@Mock
|
||||
private NetworkStatsManager mNetworkStatsManager;
|
||||
|
||||
private Context mContext;
|
||||
private DataUsageController mController;
|
||||
@@ -63,13 +76,14 @@ public class DataUsageControllerTest {
|
||||
new NetworkStatsHistory(DateUtils.DAY_IN_MILLIS /* bucketDuration */));
|
||||
doReturn(mNetworkStatsHistory)
|
||||
.when(mSession).getHistoryForNetwork(any(NetworkTemplate.class), anyInt());
|
||||
doReturn(SUB_ID).when(mTelephonyManager).getSubscriberId(anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getHistoriclUsageLevel_noNetworkSession_shouldReturn0() {
|
||||
public void getHistoricalUsageLevel_noNetworkSession_shouldReturnNegative1() {
|
||||
doReturn(null).when(mController).getSession();
|
||||
|
||||
assertThat(mController.getHistoriclUsageLevel(null /* template */)).isEqualTo(0L);
|
||||
assertThat(mController.getHistoricalUsageLevel(null /* template */)).isEqualTo(-1L);
|
||||
|
||||
}
|
||||
|
||||
@@ -77,13 +91,13 @@ public class DataUsageControllerTest {
|
||||
public void getHistoriclUsageLevel_noUsageData_shouldReturn0() {
|
||||
doReturn(mSession).when(mController).getSession();
|
||||
|
||||
assertThat(mController.getHistoriclUsageLevel(NetworkTemplate.buildTemplateWifiWildcard()))
|
||||
assertThat(mController.getHistoricalUsageLevel(NetworkTemplate.buildTemplateWifiWildcard()))
|
||||
.isEqualTo(0L);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getHistoriclUsageLevel_hasUsageData_shouldReturnTotalUsage() {
|
||||
public void getHistoricalUsageLevel_hasUsageData_shouldReturnTotalUsage() {
|
||||
doReturn(mSession).when(mController).getSession();
|
||||
final long receivedBytes = 743823454L;
|
||||
final long transmittedBytes = 16574289L;
|
||||
@@ -94,8 +108,57 @@ public class DataUsageControllerTest {
|
||||
when(mNetworkStatsHistory.getValues(eq(0L), anyLong(), anyLong(), nullable(Entry.class)))
|
||||
.thenReturn(entry);
|
||||
|
||||
assertThat(mController.getHistoriclUsageLevel(NetworkTemplate.buildTemplateWifiWildcard()))
|
||||
assertThat(mController.getHistoricalUsageLevel(NetworkTemplate.buildTemplateWifiWildcard()))
|
||||
.isEqualTo(receivedBytes + transmittedBytes);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getHistoricalUsageLevel_v2_shouldQuerySummaryForDevice() throws Exception {
|
||||
final Context context = mock(Context.class);
|
||||
FeatureFlagUtils.setEnabled(context, DataUsageController.DATA_USAGE_V2, true);
|
||||
when(context.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTelephonyManager);
|
||||
when(context.getSystemService(NetworkStatsManager.class)).thenReturn(mNetworkStatsManager);
|
||||
final DataUsageController controller = new DataUsageController(context);
|
||||
|
||||
controller.getHistoricalUsageLevel(NetworkTemplate.buildTemplateWifiWildcard());
|
||||
|
||||
verify(mNetworkStatsManager).querySummaryForDevice(eq(ConnectivityManager.TYPE_WIFI),
|
||||
eq(SUB_ID), eq(0L) /* startTime */, anyLong() /* endTime */);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getHistoricalUsageLevel_v2NoUsageData_shouldReturn0() throws Exception {
|
||||
final Context context = mock(Context.class);
|
||||
FeatureFlagUtils.setEnabled(context, DataUsageController.DATA_USAGE_V2, true);
|
||||
when(context.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTelephonyManager);
|
||||
when(context.getSystemService(NetworkStatsManager.class)).thenReturn(mNetworkStatsManager);
|
||||
when(mNetworkStatsManager.querySummaryForDevice(eq(ConnectivityManager.TYPE_WIFI),
|
||||
eq(SUB_ID), eq(0L) /* startTime */, anyLong() /* endTime */))
|
||||
.thenReturn(mock(NetworkStats.Bucket.class));
|
||||
final DataUsageController controller = new DataUsageController(context);
|
||||
|
||||
assertThat(controller.getHistoricalUsageLevel(NetworkTemplate.buildTemplateWifiWildcard()))
|
||||
.isEqualTo(0L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getHistoricalUsageLevel_v2HasUsageData_shouldReturnTotalUsage()
|
||||
throws Exception {
|
||||
final Context context = mock(Context.class);
|
||||
FeatureFlagUtils.setEnabled(context, DataUsageController.DATA_USAGE_V2, true);
|
||||
when(context.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTelephonyManager);
|
||||
when(context.getSystemService(NetworkStatsManager.class)).thenReturn(mNetworkStatsManager);
|
||||
final long receivedBytes = 743823454L;
|
||||
final long transmittedBytes = 16574289L;
|
||||
final NetworkStats.Bucket bucket = mock(NetworkStats.Bucket.class);
|
||||
when(bucket.getRxBytes()).thenReturn(receivedBytes);
|
||||
when(bucket.getTxBytes()).thenReturn(transmittedBytes);
|
||||
when(mNetworkStatsManager.querySummaryForDevice(eq(ConnectivityManager.TYPE_WIFI),
|
||||
eq(SUB_ID), eq(0L) /* startTime */, anyLong() /* endTime */)).thenReturn(bucket);
|
||||
final DataUsageController controller = new DataUsageController(context);
|
||||
|
||||
assertThat(controller.getHistoricalUsageLevel(NetworkTemplate.buildTemplateWifiWildcard()))
|
||||
.isEqualTo(receivedBytes + transmittedBytes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ public class NetworkCycleChartDataLoaderTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void recordUsage_shouldQueryNetworkSummary() throws RemoteException {
|
||||
public void recordUsage_shouldQueryNetworkSummaryForDevice() throws RemoteException {
|
||||
final long end = System.currentTimeMillis();
|
||||
final long start = end - (DateUtils.WEEK_IN_MILLIS * 4);
|
||||
final int networkType = ConnectivityManager.TYPE_MOBILE;
|
||||
@@ -68,6 +68,6 @@ public class NetworkCycleChartDataLoaderTest {
|
||||
|
||||
mLoader.recordUsage(start, end);
|
||||
|
||||
verify(mNetworkStatsManager).querySummary(networkType, subId, start, end);
|
||||
verify(mNetworkStatsManager).querySummaryForDevice(networkType, subId, start, end);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user