Merge "Add anomaly detector for unoptimized bt scanning" into oc-dr1-dev
am: 37da099df1
Change-Id: I76dccf645d50c1160010ad59607a4ffeba6a3035
This commit is contained in:
@@ -35,10 +35,12 @@ import java.util.Objects;
|
||||
public class Anomaly implements Parcelable {
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({AnomalyType.WAKE_LOCK,
|
||||
AnomalyType.WAKEUP_ALARM})
|
||||
AnomalyType.WAKEUP_ALARM,
|
||||
AnomalyType.BLUETOOTH_SCAN})
|
||||
public @interface AnomalyType {
|
||||
int WAKE_LOCK = 0;
|
||||
int WAKEUP_ALARM = 1;
|
||||
int BLUETOOTH_SCAN = 2;
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@@ -52,7 +54,8 @@ public class Anomaly implements Parcelable {
|
||||
@AnomalyType
|
||||
public static final int[] ANOMALY_TYPE_LIST =
|
||||
{AnomalyType.WAKE_LOCK,
|
||||
AnomalyType.WAKEUP_ALARM};
|
||||
AnomalyType.WAKEUP_ALARM,
|
||||
AnomalyType.BLUETOOTH_SCAN};
|
||||
|
||||
/**
|
||||
* Type of this this anomaly
|
||||
|
@@ -22,9 +22,9 @@ import android.os.BatteryStats;
|
||||
import android.os.Bundle;
|
||||
import android.os.UserManager;
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.internal.os.BatteryStatsHelper;
|
||||
import com.android.settings.Utils;
|
||||
import com.android.settings.utils.AsyncLoader;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -35,6 +35,8 @@ import java.util.List;
|
||||
* an empty list if there is no anomaly.
|
||||
*/
|
||||
public class AnomalyLoader extends AsyncLoader<List<Anomaly>> {
|
||||
private static final String TAG = "AnomalyLoader";
|
||||
|
||||
private static final boolean USE_FAKE_DATA = false;
|
||||
private BatteryStatsHelper mBatteryStatsHelper;
|
||||
private String mPackageName;
|
||||
@@ -108,9 +110,9 @@ public class AnomalyLoader extends AsyncLoader<List<Anomaly>> {
|
||||
List<Anomaly> generateFakeData() {
|
||||
final List<Anomaly> anomalies = new ArrayList<>();
|
||||
final Context context = getContext();
|
||||
final String packageName = "com.android.settings";
|
||||
final CharSequence displayName = "Settings";
|
||||
try {
|
||||
final String packageName = "com.android.settings";
|
||||
final CharSequence displayName = "Settings";
|
||||
final int uid = context.getPackageManager().getPackageUid(packageName, 0);
|
||||
|
||||
anomalies.add(new Anomaly.Builder()
|
||||
@@ -125,8 +127,14 @@ public class AnomalyLoader extends AsyncLoader<List<Anomaly>> {
|
||||
.setPackageName(packageName)
|
||||
.setDisplayName(displayName)
|
||||
.build());
|
||||
anomalies.add(new Anomaly.Builder()
|
||||
.setUid(uid)
|
||||
.setType(Anomaly.AnomalyType.BLUETOOTH_SCAN)
|
||||
.setPackageName(packageName)
|
||||
.setDisplayName(displayName)
|
||||
.build());
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Cannot find package by name: " + packageName, e);
|
||||
}
|
||||
return anomalies;
|
||||
}
|
||||
|
@@ -23,6 +23,7 @@ import com.android.settings.fuelgauge.anomaly.action.AnomalyAction;
|
||||
import com.android.settings.fuelgauge.anomaly.action.BackgroundCheckAction;
|
||||
import com.android.settings.fuelgauge.anomaly.action.ForceStopAction;
|
||||
import com.android.settings.fuelgauge.anomaly.checker.AnomalyDetector;
|
||||
import com.android.settings.fuelgauge.anomaly.checker.BluetoothScanAnomalyDetector;
|
||||
import com.android.settings.fuelgauge.anomaly.checker.WakeLockAnomalyDetector;
|
||||
import com.android.settings.fuelgauge.anomaly.checker.WakeupAlarmAnomalyDetector;
|
||||
|
||||
@@ -56,6 +57,7 @@ public class AnomalyUtils {
|
||||
case Anomaly.AnomalyType.WAKE_LOCK:
|
||||
return new ForceStopAction(mContext);
|
||||
case Anomaly.AnomalyType.WAKEUP_ALARM:
|
||||
case Anomaly.AnomalyType.BLUETOOTH_SCAN:
|
||||
return new BackgroundCheckAction(mContext);
|
||||
default:
|
||||
return null;
|
||||
@@ -74,6 +76,8 @@ public class AnomalyUtils {
|
||||
return new WakeLockAnomalyDetector(mContext);
|
||||
case Anomaly.AnomalyType.WAKEUP_ALARM:
|
||||
return new WakeupAlarmAnomalyDetector(mContext);
|
||||
case Anomaly.AnomalyType.BLUETOOTH_SCAN:
|
||||
return new BluetoothScanAnomalyDetector(mContext);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.fuelgauge.anomaly.checker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.BatteryStats;
|
||||
import android.os.SystemClock;
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.ArrayMap;
|
||||
|
||||
import com.android.internal.os.BatterySipper;
|
||||
import com.android.internal.os.BatteryStatsHelper;
|
||||
import com.android.settings.Utils;
|
||||
import com.android.settings.fuelgauge.BatteryUtils;
|
||||
import com.android.settings.fuelgauge.anomaly.Anomaly;
|
||||
import com.android.settings.fuelgauge.anomaly.AnomalyDetectionPolicy;
|
||||
import com.android.settings.fuelgauge.anomaly.AnomalyUtils;
|
||||
import com.android.settings.fuelgauge.anomaly.action.AnomalyAction;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Check whether apps have unoptimized bluetooth scanning in the background
|
||||
*/
|
||||
public class BluetoothScanAnomalyDetector implements AnomalyDetector {
|
||||
private static final String TAG = "BluetoothScanAnomalyDetector";
|
||||
@VisibleForTesting
|
||||
BatteryUtils mBatteryUtils;
|
||||
@VisibleForTesting
|
||||
AnomalyAction mAnomalyAction;
|
||||
private long mBluetoothScanningThreshold;
|
||||
private Context mContext;
|
||||
|
||||
public BluetoothScanAnomalyDetector(Context context) {
|
||||
this(context, new AnomalyDetectionPolicy(context));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
BluetoothScanAnomalyDetector(Context context, AnomalyDetectionPolicy policy) {
|
||||
mContext = context;
|
||||
mBatteryUtils = BatteryUtils.getInstance(context);
|
||||
mAnomalyAction = AnomalyUtils.getInstance(context).getAnomalyAction(
|
||||
Anomaly.AnomalyType.BLUETOOTH_SCAN);
|
||||
//TODO(b/36921532): hook up it to AnomalyDectionPolicy
|
||||
mBluetoothScanningThreshold = 30 * DateUtils.MINUTE_IN_MILLIS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Anomaly> detectAnomalies(BatteryStatsHelper batteryStatsHelper) {
|
||||
// Detect all apps if targetPackageName is null
|
||||
return detectAnomalies(batteryStatsHelper, null /* targetPackageName */);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Anomaly> detectAnomalies(BatteryStatsHelper batteryStatsHelper,
|
||||
String targetPackageName) {
|
||||
final List<BatterySipper> batterySippers = batteryStatsHelper.getUsageList();
|
||||
final List<Anomaly> anomalies = new ArrayList<>();
|
||||
final int targetUid = mBatteryUtils.getPackageUid(targetPackageName);
|
||||
final long elapsedRealtimeMs = SystemClock.elapsedRealtime();
|
||||
|
||||
for (int i = 0, size = batterySippers.size(); i < size; i++) {
|
||||
final BatterySipper sipper = batterySippers.get(i);
|
||||
final BatteryStats.Uid uid = sipper.uidObj;
|
||||
if (uid == null
|
||||
|| mBatteryUtils.shouldHideSipper(sipper)
|
||||
|| (targetUid != BatteryUtils.UID_NULL && targetUid != uid.getUid())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final long bluetoothTimeMs = getBluetoothUnoptimizedBgTimeMs(uid, elapsedRealtimeMs);
|
||||
if (bluetoothTimeMs > mBluetoothScanningThreshold) {
|
||||
final String packageName = mBatteryUtils.getPackageName(uid.getUid());
|
||||
final CharSequence displayName = Utils.getApplicationLabel(mContext,
|
||||
packageName);
|
||||
|
||||
Anomaly anomaly = new Anomaly.Builder()
|
||||
.setUid(uid.getUid())
|
||||
.setType(Anomaly.AnomalyType.BLUETOOTH_SCAN)
|
||||
.setDisplayName(displayName)
|
||||
.setPackageName(packageName)
|
||||
.build();
|
||||
|
||||
if (mAnomalyAction.isActionActive(anomaly)) {
|
||||
anomalies.add(anomaly);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return anomalies;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public long getBluetoothUnoptimizedBgTimeMs(BatteryStats.Uid uid, long elapsedRealtimeMs) {
|
||||
BatteryStats.Timer timer = uid.getBluetoothUnoptimizedScanBackgroundTimer();
|
||||
|
||||
return timer != null ? timer.getTotalDurationMsLocked(elapsedRealtimeMs) : 0;
|
||||
}
|
||||
|
||||
}
|
@@ -65,7 +65,8 @@ public class WakeLockAnomalyDetector implements AnomalyDetector {
|
||||
|
||||
@Override
|
||||
public List<Anomaly> detectAnomalies(BatteryStatsHelper batteryStatsHelper) {
|
||||
return detectAnomalies(batteryStatsHelper, null);
|
||||
// Detect all apps if targetPackageName is null
|
||||
return detectAnomalies(batteryStatsHelper, null /* targetPackageName */);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -61,7 +61,8 @@ public class WakeupAlarmAnomalyDetector implements AnomalyDetector {
|
||||
|
||||
@Override
|
||||
public List<Anomaly> detectAnomalies(BatteryStatsHelper batteryStatsHelper) {
|
||||
return detectAnomalies(batteryStatsHelper, null);
|
||||
// Detect all apps if targetPackageName is null
|
||||
return detectAnomalies(batteryStatsHelper, null /* targetPackageName */);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -30,6 +30,7 @@ import android.content.pm.PackageManager;
|
||||
import android.os.UserManager;
|
||||
|
||||
import com.android.internal.os.BatteryStatsHelper;
|
||||
import com.android.settings.fuelgauge.anomaly.checker.BluetoothScanAnomalyDetector;
|
||||
import com.android.settings.testutils.SettingsRobolectricTestRunner;
|
||||
import com.android.settings.TestConfig;
|
||||
import com.android.settings.fuelgauge.anomaly.checker.WakeLockAnomalyDetector;
|
||||
@@ -62,13 +63,17 @@ public class AnomalyLoaderTest {
|
||||
@Mock
|
||||
private WakeupAlarmAnomalyDetector mWakeupAlarmAnomalyDetector;
|
||||
@Mock
|
||||
private BluetoothScanAnomalyDetector mBluetoothScanAnomalyDetector;
|
||||
@Mock
|
||||
private AnomalyDetectionPolicy mAnomalyDetectionPolicy;
|
||||
@Mock
|
||||
private UserManager mUserManager;
|
||||
private Anomaly mWakeLockAnomaly;
|
||||
private Anomaly mWakeupAlarmAnomaly;
|
||||
private Anomaly mBluetoothScanAnomaly;
|
||||
private List<Anomaly> mWakeLockAnomalies;
|
||||
private List<Anomaly> mWakeupAlarmAnomalies;
|
||||
private List<Anomaly> mBluetoothScanAnomalies;
|
||||
private AnomalyLoader mAnomalyLoader;
|
||||
|
||||
@Before
|
||||
@@ -91,6 +96,12 @@ public class AnomalyLoaderTest {
|
||||
doReturn(mWakeupAlarmAnomalies).when(mWakeupAlarmAnomalyDetector).detectAnomalies(any(),
|
||||
any());
|
||||
|
||||
mBluetoothScanAnomalies = new ArrayList<>();
|
||||
mBluetoothScanAnomaly = createAnomaly(Anomaly.AnomalyType.BLUETOOTH_SCAN);
|
||||
mBluetoothScanAnomalies.add(mBluetoothScanAnomaly);
|
||||
doReturn(mBluetoothScanAnomalies).when(mBluetoothScanAnomalyDetector).detectAnomalies(any(),
|
||||
any());
|
||||
|
||||
mAnomalyLoader = new AnomalyLoader(mContext, mBatteryStatsHelper, null,
|
||||
mAnomalyDetectionPolicy);
|
||||
mAnomalyLoader.mAnomalyUtils = spy(new AnomalyUtils(mContext));
|
||||
@@ -102,10 +113,14 @@ public class AnomalyLoaderTest {
|
||||
Anomaly.AnomalyType.WAKE_LOCK);
|
||||
doReturn(mWakeupAlarmAnomalyDetector).when(mAnomalyLoader.mAnomalyUtils).getAnomalyDetector(
|
||||
Anomaly.AnomalyType.WAKEUP_ALARM);
|
||||
doReturn(mBluetoothScanAnomalyDetector).when(
|
||||
mAnomalyLoader.mAnomalyUtils).getAnomalyDetector(
|
||||
Anomaly.AnomalyType.BLUETOOTH_SCAN);
|
||||
|
||||
List<Anomaly> anomalies = mAnomalyLoader.loadInBackground();
|
||||
|
||||
assertThat(anomalies).containsExactly(mWakeLockAnomaly, mWakeupAlarmAnomaly);
|
||||
assertThat(anomalies).containsExactly(mWakeLockAnomaly, mWakeupAlarmAnomaly,
|
||||
mBluetoothScanAnomaly);
|
||||
}
|
||||
|
||||
private Anomaly createAnomaly(@Anomaly.AnomalyType int type) {
|
||||
@@ -121,6 +136,7 @@ public class AnomalyLoaderTest {
|
||||
public void testGenerateFakeData() {
|
||||
List<Anomaly> anomalies = mAnomalyLoader.generateFakeData();
|
||||
|
||||
assertThat(anomalies).containsExactly(mWakeLockAnomaly, mWakeupAlarmAnomaly);
|
||||
assertThat(anomalies).containsExactly(mWakeLockAnomaly, mWakeupAlarmAnomaly,
|
||||
mBluetoothScanAnomaly);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.fuelgauge.anomaly.checker;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.nullable;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyLong;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.spy;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.os.BatteryStats;
|
||||
import android.text.format.DateUtils;
|
||||
|
||||
import com.android.internal.os.BatterySipper;
|
||||
import com.android.internal.os.BatteryStatsHelper;
|
||||
import com.android.settings.TestConfig;
|
||||
import com.android.settings.fuelgauge.BatteryUtils;
|
||||
import com.android.settings.fuelgauge.anomaly.Anomaly;
|
||||
import com.android.settings.fuelgauge.anomaly.AnomalyDetectionPolicy;
|
||||
import com.android.settings.fuelgauge.anomaly.action.AnomalyAction;
|
||||
import com.android.settings.testutils.SettingsRobolectricTestRunner;
|
||||
|
||||
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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@RunWith(SettingsRobolectricTestRunner.class)
|
||||
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
|
||||
public class BluetoothScanAnomalyDetectorTest {
|
||||
private static final String TARGET_PACKAGE_NAME = "com.android.app";
|
||||
private static final int ANOMALY_UID = 111;
|
||||
private static final int NORMAL_UID = 222;
|
||||
private static final int TARGET_UID = 333;
|
||||
private static final long ANOMALY_BLUETOOTH_SCANNING_TIME = DateUtils.HOUR_IN_MILLIS;
|
||||
private static final long NORMAL_BLUETOOTH_SCANNING_TIME = DateUtils.MINUTE_IN_MILLIS;
|
||||
@Mock
|
||||
private BatteryStatsHelper mBatteryStatsHelper;
|
||||
@Mock
|
||||
private BatterySipper mAnomalySipper;
|
||||
@Mock
|
||||
private BatterySipper mNormalSipper;
|
||||
@Mock
|
||||
private BatterySipper mTargetSipper;
|
||||
@Mock
|
||||
private BatteryStats.Uid mAnomalyUid;
|
||||
@Mock
|
||||
private BatteryStats.Uid mNormalUid;
|
||||
@Mock
|
||||
private BatteryStats.Uid mTargetUid;
|
||||
@Mock
|
||||
private BatteryUtils mBatteryUtils;
|
||||
@Mock
|
||||
private AnomalyDetectionPolicy mPolicy;
|
||||
@Mock
|
||||
private AnomalyAction mAnomalyAction;
|
||||
|
||||
private BluetoothScanAnomalyDetector mBluetoothScanAnomalyDetector;
|
||||
private Context mContext;
|
||||
private List<BatterySipper> mUsageList;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
mContext = spy(RuntimeEnvironment.application);
|
||||
|
||||
mAnomalySipper.uidObj = mAnomalyUid;
|
||||
doReturn(ANOMALY_UID).when(mAnomalyUid).getUid();
|
||||
mNormalSipper.uidObj = mNormalUid;
|
||||
doReturn(NORMAL_UID).when(mNormalUid).getUid();
|
||||
mTargetSipper.uidObj = mTargetUid;
|
||||
doReturn(TARGET_UID).when(mTargetUid).getUid();
|
||||
|
||||
mUsageList = new ArrayList<>();
|
||||
mUsageList.add(mAnomalySipper);
|
||||
mUsageList.add(mNormalSipper);
|
||||
mUsageList.add(mTargetSipper);
|
||||
doReturn(mUsageList).when(mBatteryStatsHelper).getUsageList();
|
||||
|
||||
mBluetoothScanAnomalyDetector = spy(new BluetoothScanAnomalyDetector(mContext, mPolicy));
|
||||
mBluetoothScanAnomalyDetector.mBatteryUtils = mBatteryUtils;
|
||||
mBluetoothScanAnomalyDetector.mAnomalyAction = mAnomalyAction;
|
||||
doReturn(false).when(mBatteryUtils).shouldHideSipper(any());
|
||||
doReturn(true).when(mAnomalyAction).isActionActive(any());
|
||||
|
||||
doReturn(ANOMALY_BLUETOOTH_SCANNING_TIME).when(
|
||||
mBluetoothScanAnomalyDetector).getBluetoothUnoptimizedBgTimeMs(eq(mAnomalyUid),
|
||||
anyLong());
|
||||
doReturn(ANOMALY_BLUETOOTH_SCANNING_TIME).when(
|
||||
mBluetoothScanAnomalyDetector).getBluetoothUnoptimizedBgTimeMs(eq(mTargetUid),
|
||||
anyLong());
|
||||
doReturn(NORMAL_BLUETOOTH_SCANNING_TIME).when(
|
||||
mBluetoothScanAnomalyDetector).getBluetoothUnoptimizedBgTimeMs(eq(mNormalUid),
|
||||
anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDetectAnomalies_containsAnomaly_detectIt() {
|
||||
doReturn(-1).when(mBatteryUtils).getPackageUid(nullable(String.class));
|
||||
final Anomaly anomaly = createBluetoothAnomaly(ANOMALY_UID);
|
||||
final Anomaly targetAnomaly = createBluetoothAnomaly(TARGET_UID);
|
||||
|
||||
List<Anomaly> mAnomalies = mBluetoothScanAnomalyDetector.detectAnomalies(
|
||||
mBatteryStatsHelper);
|
||||
|
||||
assertThat(mAnomalies).containsExactly(anomaly, targetAnomaly);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDetectAnomalies_detectTargetAnomaly_detectIt() {
|
||||
doReturn(TARGET_UID).when(mBatteryUtils).getPackageUid(TARGET_PACKAGE_NAME);
|
||||
final Anomaly targetAnomaly = createBluetoothAnomaly(TARGET_UID);
|
||||
|
||||
List<Anomaly> mAnomalies = mBluetoothScanAnomalyDetector.detectAnomalies(
|
||||
mBatteryStatsHelper, TARGET_PACKAGE_NAME);
|
||||
|
||||
assertThat(mAnomalies).containsExactly(targetAnomaly);
|
||||
|
||||
}
|
||||
|
||||
private Anomaly createBluetoothAnomaly(int uid) {
|
||||
return new Anomaly.Builder()
|
||||
.setUid(uid)
|
||||
.setType(Anomaly.AnomalyType.BLUETOOTH_SCAN)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user