Add DataProcessor to process raw history map for UI.
- Move interpolation from DatabaseUtil - Add the logic to wrap processed history map to BatteryLevelData - Change ImmutableList to List in BatteryLevelData because ImmutableList.of doesn't support null element. Bug: 236101687 Test: make RunSettingsRoboTests Change-Id: I0a842f24e5481faf65f667c08a4fdc11fe2f235c
This commit is contained in:
@@ -0,0 +1,454 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.batteryusage;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.Mockito.spy;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.text.format.DateUtils;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class DataProcessorTest {
|
||||
private static final String FAKE_ENTRY_KEY = "fake_entry_key";
|
||||
|
||||
private Context mContext;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));
|
||||
|
||||
mContext = spy(RuntimeEnvironment.application);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getBatteryLevelData_emptyHistoryMap_returnNull() {
|
||||
assertThat(DataProcessor.getBatteryLevelData(mContext, null)).isNull();
|
||||
assertThat(DataProcessor.getBatteryLevelData(mContext, new HashMap<>())).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getBatteryLevelData_notEnoughData_returnNull() {
|
||||
// The timestamps are within 1 hour.
|
||||
final long[] timestamps = {1000000L, 2000000L, 3000000L};
|
||||
final int[] levels = {100, 99, 98};
|
||||
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
|
||||
createHistoryMap(timestamps, levels);
|
||||
|
||||
assertThat(DataProcessor.getBatteryLevelData(mContext, batteryHistoryMap)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getBatteryLevelData_returnExpectedResult() {
|
||||
// Timezone GMT+8: 2022-01-01 00:00:00, 2022-01-01 01:00:00, 2022-01-01 02:00:00
|
||||
final long[] timestamps = {1640966400000L, 1640970000000L, 1640973600000L};
|
||||
final int[] levels = {100, 99, 98};
|
||||
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
|
||||
createHistoryMap(timestamps, levels);
|
||||
|
||||
final BatteryLevelData resultData =
|
||||
DataProcessor.getBatteryLevelData(mContext, batteryHistoryMap);
|
||||
|
||||
final List<Long> expectedDailyTimestamps = List.of(timestamps[0], timestamps[2]);
|
||||
final List<Integer> expectedDailyLevels = List.of(levels[0], levels[2]);
|
||||
final List<List<Long>> expectedHourlyTimestamps = List.of(expectedDailyTimestamps);
|
||||
final List<List<Integer>> expectedHourlyLevels = List.of(expectedDailyLevels);
|
||||
verifyExpectedBatteryLevelData(
|
||||
resultData,
|
||||
expectedDailyTimestamps,
|
||||
expectedDailyLevels,
|
||||
expectedHourlyTimestamps,
|
||||
expectedHourlyLevels);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getHistoryMapWithExpectedTimestamps_emptyHistoryMap_returnEmptyMap() {
|
||||
assertThat(DataProcessor
|
||||
.getHistoryMapWithExpectedTimestamps(mContext, new HashMap<>()))
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getHistoryMapWithExpectedTimestamps_returnExpectedMap() {
|
||||
// Timezone GMT+8
|
||||
final long[] timestamps = {
|
||||
1640966700000L, // 2022-01-01 00:05:00
|
||||
1640970180000L, // 2022-01-01 01:03:00
|
||||
1640973840000L, // 2022-01-01 02:04:00
|
||||
1640978100000L, // 2022-01-01 03:15:00
|
||||
1640981400000L // 2022-01-01 04:10:00
|
||||
};
|
||||
final int[] levels = {100, 94, 90, 82, 50};
|
||||
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
|
||||
createHistoryMap(timestamps, levels);
|
||||
|
||||
final Map<Long, Map<String, BatteryHistEntry>> resultMap =
|
||||
DataProcessor.getHistoryMapWithExpectedTimestamps(mContext, batteryHistoryMap);
|
||||
|
||||
// Timezone GMT+8
|
||||
final long[] expectedTimestamps = {
|
||||
1640966400000L, // 2022-01-01 00:00:00
|
||||
1640970000000L, // 2022-01-01 01:00:00
|
||||
1640973600000L, // 2022-01-01 02:00:00
|
||||
1640977200000L, // 2022-01-01 03:00:00
|
||||
1640980800000L // 2022-01-01 04:00:00
|
||||
};
|
||||
final int[] expectedLevels = {100, 94, 90, 84, 56};
|
||||
assertThat(resultMap).hasSize(expectedLevels.length);
|
||||
for (int index = 0; index < expectedLevels.length; index++) {
|
||||
assertThat(resultMap.get(expectedTimestamps[index]).get(FAKE_ENTRY_KEY).mBatteryLevel)
|
||||
.isEqualTo(expectedLevels[index]);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getLevelDataThroughProcessedHistoryMap_notEnoughData_returnNull() {
|
||||
final long[] timestamps = {100L};
|
||||
final int[] levels = {100};
|
||||
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
|
||||
createHistoryMap(timestamps, levels);
|
||||
|
||||
assertThat(
|
||||
DataProcessor.getLevelDataThroughProcessedHistoryMap(mContext, batteryHistoryMap))
|
||||
.isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getLevelDataThroughProcessedHistoryMap_OneDayData_returnExpectedResult() {
|
||||
// Timezone GMT+8
|
||||
final long[] timestamps = {
|
||||
1640966400000L, // 2022-01-01 00:00:00
|
||||
1640970000000L, // 2022-01-01 01:00:00
|
||||
1640973600000L, // 2022-01-01 02:00:00
|
||||
1640977200000L, // 2022-01-01 03:00:00
|
||||
1640980800000L // 2022-01-01 04:00:00
|
||||
};
|
||||
final int[] levels = {100, 94, 90, 82, 50};
|
||||
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
|
||||
createHistoryMap(timestamps, levels);
|
||||
|
||||
final BatteryLevelData resultData =
|
||||
DataProcessor.getLevelDataThroughProcessedHistoryMap(mContext, batteryHistoryMap);
|
||||
|
||||
final List<Long> expectedDailyTimestamps = List.of(timestamps[0], timestamps[4]);
|
||||
final List<Integer> expectedDailyLevels = List.of(levels[0], levels[4]);
|
||||
final List<List<Long>> expectedHourlyTimestamps = List.of(
|
||||
List.of(timestamps[0], timestamps[2], timestamps[4])
|
||||
);
|
||||
final List<List<Integer>> expectedHourlyLevels = List.of(
|
||||
List.of(levels[0], levels[2], levels[4])
|
||||
);
|
||||
verifyExpectedBatteryLevelData(
|
||||
resultData,
|
||||
expectedDailyTimestamps,
|
||||
expectedDailyLevels,
|
||||
expectedHourlyTimestamps,
|
||||
expectedHourlyLevels);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getLevelDataThroughProcessedHistoryMap_MultipleDaysData_returnExpectedResult() {
|
||||
// Timezone GMT+8
|
||||
final long[] timestamps = {
|
||||
1641038400000L, // 2022-01-01 20:00:00
|
||||
1641060000000L, // 2022-01-02 02:00:00
|
||||
1641067200000L, // 2022-01-02 04:00:00
|
||||
1641081600000L, // 2022-01-02 08:00:00
|
||||
};
|
||||
final int[] levels = {100, 94, 90, 82};
|
||||
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
|
||||
createHistoryMap(timestamps, levels);
|
||||
|
||||
final BatteryLevelData resultData =
|
||||
DataProcessor.getLevelDataThroughProcessedHistoryMap(mContext, batteryHistoryMap);
|
||||
|
||||
final List<Long> expectedDailyTimestamps = List.of(
|
||||
1641038400000L, // 2022-01-01 20:00:00
|
||||
1641052800000L, // 2022-01-02 00:00:00
|
||||
1641081600000L // 2022-01-02 08:00:00
|
||||
);
|
||||
final List<Integer> expectedDailyLevels = new ArrayList<>();
|
||||
expectedDailyLevels.add(100);
|
||||
expectedDailyLevels.add(null);
|
||||
expectedDailyLevels.add(82);
|
||||
final List<List<Long>> expectedHourlyTimestamps = List.of(
|
||||
List.of(
|
||||
1641038400000L, // 2022-01-01 20:00:00
|
||||
1641045600000L, // 2022-01-01 22:00:00
|
||||
1641052800000L // 2022-01-02 00:00:00
|
||||
),
|
||||
List.of(
|
||||
1641052800000L, // 2022-01-02 00:00:00
|
||||
1641060000000L, // 2022-01-02 02:00:00
|
||||
1641067200000L, // 2022-01-02 04:00:00
|
||||
1641074400000L, // 2022-01-02 06:00:00
|
||||
1641081600000L // 2022-01-02 08:00:00
|
||||
)
|
||||
);
|
||||
final List<Integer> expectedHourlyLevels1 = new ArrayList<>();
|
||||
expectedHourlyLevels1.add(100);
|
||||
expectedHourlyLevels1.add(null);
|
||||
expectedHourlyLevels1.add(null);
|
||||
final List<Integer> expectedHourlyLevels2 = new ArrayList<>();
|
||||
expectedHourlyLevels2.add(null);
|
||||
expectedHourlyLevels2.add(94);
|
||||
expectedHourlyLevels2.add(90);
|
||||
expectedHourlyLevels2.add(null);
|
||||
expectedHourlyLevels2.add(82);
|
||||
final List<List<Integer>> expectedHourlyLevels = List.of(
|
||||
expectedHourlyLevels1,
|
||||
expectedHourlyLevels2
|
||||
);
|
||||
verifyExpectedBatteryLevelData(
|
||||
resultData,
|
||||
expectedDailyTimestamps,
|
||||
expectedDailyLevels,
|
||||
expectedHourlyTimestamps,
|
||||
expectedHourlyLevels);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTimestampSlots_emptyRawList_returnEmptyList() {
|
||||
final List<Long> resultList =
|
||||
DataProcessor.getTimestampSlots(new ArrayList<>());
|
||||
assertThat(resultList).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTimestampSlots_startWithEvenHour_returnExpectedResult() {
|
||||
final Calendar startCalendar = Calendar.getInstance();
|
||||
startCalendar.set(2022, 6, 5, 6, 30, 50); // 2022-07-05 06:30:50
|
||||
final Calendar endCalendar = Calendar.getInstance();
|
||||
endCalendar.set(2022, 6, 5, 22, 30, 50); // 2022-07-05 22:30:50
|
||||
|
||||
final Calendar expectedStartCalendar = Calendar.getInstance();
|
||||
expectedStartCalendar.set(2022, 6, 5, 6, 0, 0); // 2022-07-05 06:00:00
|
||||
final Calendar expectedEndCalendar = Calendar.getInstance();
|
||||
expectedEndCalendar.set(2022, 6, 5, 22, 0, 0); // 2022-07-05 22:00:00
|
||||
verifyExpectedTimestampSlots(
|
||||
startCalendar, endCalendar, expectedStartCalendar, expectedEndCalendar);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTimestampSlots_startWithOddHour_returnExpectedResult() {
|
||||
final Calendar startCalendar = Calendar.getInstance();
|
||||
startCalendar.set(2022, 6, 5, 5, 0, 50); // 2022-07-05 05:00:50
|
||||
final Calendar endCalendar = Calendar.getInstance();
|
||||
endCalendar.set(2022, 6, 6, 21, 00, 50); // 2022-07-06 21:00:50
|
||||
|
||||
final Calendar expectedStartCalendar = Calendar.getInstance();
|
||||
expectedStartCalendar.set(2022, 6, 5, 6, 00, 00); // 2022-07-05 06:00:00
|
||||
final Calendar expectedEndCalendar = Calendar.getInstance();
|
||||
expectedEndCalendar.set(2022, 6, 6, 20, 00, 00); // 2022-07-06 20:00:00
|
||||
verifyExpectedTimestampSlots(
|
||||
startCalendar, endCalendar, expectedStartCalendar, expectedEndCalendar);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getDailyTimestamps_notEnoughData_returnEmptyList() {
|
||||
assertThat(DataProcessor.getDailyTimestamps(new ArrayList<>())).isEmpty();
|
||||
assertThat(DataProcessor.getDailyTimestamps(List.of(100L))).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getDailyTimestamps_OneDayData_returnExpectedList() {
|
||||
// Timezone GMT+8
|
||||
final List<Long> timestamps = List.of(
|
||||
1640966400000L, // 2022-01-01 00:00:00
|
||||
1640970000000L, // 2022-01-01 01:00:00
|
||||
1640973600000L, // 2022-01-01 02:00:00
|
||||
1640977200000L, // 2022-01-01 03:00:00
|
||||
1640980800000L // 2022-01-01 04:00:00
|
||||
);
|
||||
|
||||
final List<Long> expectedTimestamps = List.of(
|
||||
1640966400000L, // 2022-01-01 00:00:00
|
||||
1640980800000L // 2022-01-01 04:00:00
|
||||
);
|
||||
assertThat(DataProcessor.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getDailyTimestamps_MultipleDaysData_returnExpectedList() {
|
||||
// Timezone GMT+8
|
||||
final List<Long> timestamps = List.of(
|
||||
1640988000000L, // 2022-01-01 06:00:00
|
||||
1641060000000L, // 2022-01-02 02:00:00
|
||||
1641160800000L, // 2022-01-03 06:00:00
|
||||
1641254400000L // 2022-01-04 08:00:00
|
||||
);
|
||||
|
||||
final List<Long> expectedTimestamps = List.of(
|
||||
1640988000000L, // 2022-01-01 06:00:00
|
||||
1641052800000L, // 2022-01-02 00:00:00
|
||||
1641139200000L, // 2022-01-03 00:00:00
|
||||
1641225600000L, // 2022-01-04 00:00:00
|
||||
1641254400000L // 2022-01-04 08:00:00
|
||||
);
|
||||
assertThat(DataProcessor.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isFromFullCharge_emptyData_returnFalse() {
|
||||
assertThat(DataProcessor.isFromFullCharge(null)).isFalse();
|
||||
assertThat(DataProcessor.isFromFullCharge(new HashMap<>())).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isFromFullCharge_notChargedData_returnFalse() {
|
||||
final Map<String, BatteryHistEntry> entryMap = new HashMap<>();
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put("batteryLevel", 98);
|
||||
final BatteryHistEntry entry = new BatteryHistEntry(values);
|
||||
entryMap.put(FAKE_ENTRY_KEY, entry);
|
||||
|
||||
assertThat(DataProcessor.isFromFullCharge(entryMap)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isFromFullCharge_chargedData_returnTrue() {
|
||||
final Map<String, BatteryHistEntry> entryMap = new HashMap<>();
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put("batteryLevel", 100);
|
||||
final BatteryHistEntry entry = new BatteryHistEntry(values);
|
||||
entryMap.put(FAKE_ENTRY_KEY, entry);
|
||||
|
||||
assertThat(DataProcessor.isFromFullCharge(entryMap)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findNearestTimestamp_returnExpectedResult() {
|
||||
long[] results = DataProcessor.findNearestTimestamp(
|
||||
Arrays.asList(10L, 20L, 30L, 40L), /*target=*/ 15L);
|
||||
assertThat(results).isEqualTo(new long[] {10L, 20L});
|
||||
|
||||
results = DataProcessor.findNearestTimestamp(
|
||||
Arrays.asList(10L, 20L, 30L, 40L), /*target=*/ 10L);
|
||||
assertThat(results).isEqualTo(new long[] {10L, 10L});
|
||||
|
||||
results = DataProcessor.findNearestTimestamp(
|
||||
Arrays.asList(10L, 20L, 30L, 40L), /*target=*/ 5L);
|
||||
assertThat(results).isEqualTo(new long[] {0L, 10L});
|
||||
|
||||
results = DataProcessor.findNearestTimestamp(
|
||||
Arrays.asList(10L, 20L, 30L, 40L), /*target=*/ 50L);
|
||||
assertThat(results).isEqualTo(new long[] {40L, 0L});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getTimestampOfNextDay_returnExpectedResult() {
|
||||
// 2021-02-28 06:00:00 => 2021-03-01 00:00:00
|
||||
assertThat(DataProcessor.getTimestampOfNextDay(1614463200000L))
|
||||
.isEqualTo(1614528000000L);
|
||||
// 2021-12-31 16:00:00 => 2022-01-01 00:00:00
|
||||
assertThat(DataProcessor.getTimestampOfNextDay(1640937600000L))
|
||||
.isEqualTo(1640966400000L);
|
||||
}
|
||||
|
||||
private static Map<Long, Map<String, BatteryHistEntry>> createHistoryMap(
|
||||
final long[] timestamps, final int[] levels) {
|
||||
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap = new HashMap<>();
|
||||
for (int index = 0; index < timestamps.length; index++) {
|
||||
final Map<String, BatteryHistEntry> entryMap = new HashMap<>();
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(BatteryHistEntry.KEY_BATTERY_LEVEL, levels[index]);
|
||||
final BatteryHistEntry entry = new BatteryHistEntry(values);
|
||||
entryMap.put(FAKE_ENTRY_KEY, entry);
|
||||
batteryHistoryMap.put(timestamps[index], entryMap);
|
||||
}
|
||||
return batteryHistoryMap;
|
||||
}
|
||||
|
||||
private static void verifyExpectedBatteryLevelData(
|
||||
final BatteryLevelData resultData,
|
||||
final List<Long> expectedDailyTimestamps,
|
||||
final List<Integer> expectedDailyLevels,
|
||||
final List<List<Long>> expectedHourlyTimestamps,
|
||||
final List<List<Integer>> expectedHourlyLevels) {
|
||||
final BatteryLevelData.PeriodBatteryLevelData dailyResultData =
|
||||
resultData.getDailyBatteryLevels();
|
||||
final List<BatteryLevelData.PeriodBatteryLevelData> hourlyResultData =
|
||||
resultData.getHourlyBatteryLevelsPerDay();
|
||||
verifyExpectedDailyBatteryLevelData(
|
||||
dailyResultData, expectedDailyTimestamps, expectedDailyLevels);
|
||||
verifyExpectedHourlyBatteryLevelData(
|
||||
hourlyResultData, expectedHourlyTimestamps, expectedHourlyLevels);
|
||||
}
|
||||
|
||||
private static void verifyExpectedDailyBatteryLevelData(
|
||||
final BatteryLevelData.PeriodBatteryLevelData dailyResultData,
|
||||
final List<Long> expectedDailyTimestamps,
|
||||
final List<Integer> expectedDailyLevels) {
|
||||
assertThat(dailyResultData.getTimestamps()).isEqualTo(expectedDailyTimestamps);
|
||||
assertThat(dailyResultData.getLevels()).isEqualTo(expectedDailyLevels);
|
||||
}
|
||||
|
||||
private static void verifyExpectedHourlyBatteryLevelData(
|
||||
final List<BatteryLevelData.PeriodBatteryLevelData> hourlyResultData,
|
||||
final List<List<Long>> expectedHourlyTimestamps,
|
||||
final List<List<Integer>> expectedHourlyLevels) {
|
||||
final int expectedHourlySize = expectedHourlyTimestamps.size();
|
||||
assertThat(hourlyResultData).hasSize(expectedHourlySize);
|
||||
for (int dailyIndex = 0; dailyIndex < expectedHourlySize; dailyIndex++) {
|
||||
assertThat(hourlyResultData.get(dailyIndex).getTimestamps())
|
||||
.isEqualTo(expectedHourlyTimestamps.get(dailyIndex));
|
||||
assertThat(hourlyResultData.get(dailyIndex).getLevels())
|
||||
.isEqualTo(expectedHourlyLevels.get(dailyIndex));
|
||||
}
|
||||
}
|
||||
|
||||
private static void verifyExpectedTimestampSlots(
|
||||
final Calendar start,
|
||||
final Calendar end,
|
||||
final Calendar expectedStart,
|
||||
final Calendar expectedEnd) {
|
||||
expectedStart.set(Calendar.MILLISECOND, 0);
|
||||
expectedEnd.set(Calendar.MILLISECOND, 0);
|
||||
final ArrayList<Long> timestampSlots = new ArrayList<>();
|
||||
timestampSlots.add(start.getTimeInMillis());
|
||||
timestampSlots.add(end.getTimeInMillis());
|
||||
final List<Long> resultList =
|
||||
DataProcessor.getTimestampSlots(timestampSlots);
|
||||
|
||||
for (int index = 0; index < resultList.size(); index++) {
|
||||
final long expectedTimestamp =
|
||||
expectedStart.getTimeInMillis() + index * DateUtils.HOUR_IN_MILLIS;
|
||||
assertThat(resultList.get(index)).isEqualTo(expectedTimestamp);
|
||||
}
|
||||
assertThat(resultList.get(resultList.size() - 1))
|
||||
.isEqualTo(expectedEnd.getTimeInMillis());
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user