Merge commit '47eec246a9293616cb74d2c354be85201ec4e19e' into mergeit
Change-Id: I988ae328e0e9bb99b086490cf2ef48ca19779d13
This commit is contained in:
@@ -131,7 +131,7 @@ public class StorageDashboardFragment extends DashboardFragment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mPreferenceController.onLoadFinished(mAppsResult.get(UserHandle.myUserId()));
|
mPreferenceController.onLoadFinished(mAppsResult, UserHandle.myUserId());
|
||||||
updateSecondaryUserControllers(mSecondaryUsers, mAppsResult);
|
updateSecondaryUserControllers(mSecondaryUsers, mAppsResult);
|
||||||
|
|
||||||
// setLoading always causes a flicker, so let's avoid doing it.
|
// setLoading always causes a flicker, so let's avoid doing it.
|
||||||
|
@@ -120,7 +120,8 @@ public class StorageProfileFragment extends DashboardFragment
|
|||||||
@Override
|
@Override
|
||||||
public void onLoadFinished(Loader<SparseArray<AppsStorageResult>> loader,
|
public void onLoadFinished(Loader<SparseArray<AppsStorageResult>> loader,
|
||||||
SparseArray<AppsStorageResult> result) {
|
SparseArray<AppsStorageResult> result) {
|
||||||
mPreferenceController.onLoadFinished(scrubAppsFromResult(result.get(mUserId)));
|
scrubAppsFromResult(result.get(mUserId));
|
||||||
|
mPreferenceController.onLoadFinished(result, mUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@@ -22,8 +22,8 @@ import static android.content.pm.ApplicationInfo.CATEGORY_VIDEO;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.pm.ApplicationInfo;
|
import android.content.pm.ApplicationInfo;
|
||||||
import android.content.pm.UserInfo;
|
|
||||||
import android.content.pm.PackageManager.NameNotFoundException;
|
import android.content.pm.PackageManager.NameNotFoundException;
|
||||||
|
import android.content.pm.UserInfo;
|
||||||
import android.os.UserHandle;
|
import android.os.UserHandle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
@@ -87,44 +87,43 @@ public class StorageAsyncLoader
|
|||||||
stats = mStatsManager.getStatsForPackage(mUuid, app.packageName, myUser);
|
stats = mStatsManager.getStatsForPackage(mUuid, app.packageName, myUser);
|
||||||
} catch (NameNotFoundException | IOException e) {
|
} catch (NameNotFoundException | IOException e) {
|
||||||
// This may happen if the package was removed during our calculation.
|
// This may happen if the package was removed during our calculation.
|
||||||
|
Log.w(TAG, "App unexpectedly not found", e);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
long attributedAppSizeInBytes = stats.getDataBytes();
|
long blamedSize = stats.getDataBytes() - stats.getCacheBytes();
|
||||||
// This matches how the package manager calculates sizes -- by zeroing out code sizes of
|
|
||||||
// system apps which are not updated. My initial tests suggest that this results in the
|
// Only count app code against the current user; we don't want
|
||||||
// original code size being counted for updated system apps when they shouldn't, but
|
// double-counting on multi-user devices.
|
||||||
// I am not sure how to avoid this problem without specifically going in to find that
|
if (userId == UserHandle.myUserId()) {
|
||||||
// code size.
|
blamedSize += stats.getCodeBytes();
|
||||||
if (!app.isSystemApp() || app.isUpdatedSystemApp()) {
|
|
||||||
attributedAppSizeInBytes += stats.getCodeBytes();
|
|
||||||
} else {
|
|
||||||
result.systemSize += stats.getCodeBytes();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (app.category) {
|
switch (app.category) {
|
||||||
case CATEGORY_GAME:
|
case CATEGORY_GAME:
|
||||||
result.gamesSize += attributedAppSizeInBytes;
|
result.gamesSize += blamedSize;
|
||||||
break;
|
break;
|
||||||
case CATEGORY_AUDIO:
|
case CATEGORY_AUDIO:
|
||||||
result.musicAppsSize += attributedAppSizeInBytes;
|
result.musicAppsSize += blamedSize;
|
||||||
break;
|
break;
|
||||||
case CATEGORY_VIDEO:
|
case CATEGORY_VIDEO:
|
||||||
result.videoAppsSize += attributedAppSizeInBytes;
|
result.videoAppsSize += blamedSize;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// The deprecated game flag does not set the category.
|
// The deprecated game flag does not set the category.
|
||||||
if ((app.flags & ApplicationInfo.FLAG_IS_GAME) != 0) {
|
if ((app.flags & ApplicationInfo.FLAG_IS_GAME) != 0) {
|
||||||
result.gamesSize += attributedAppSizeInBytes;
|
result.gamesSize += blamedSize;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
result.otherAppsSize += attributedAppSizeInBytes;
|
result.otherAppsSize += blamedSize;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "Loading external stats");
|
Log.d(TAG, "Loading external stats");
|
||||||
try {
|
try {
|
||||||
result.externalStats = mStatsManager.getExternalStorageStats(mUuid, UserHandle.of(userId));
|
result.externalStats = mStatsManager.getExternalStorageStats(mUuid,
|
||||||
|
UserHandle.of(userId));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
}
|
}
|
||||||
@@ -141,7 +140,6 @@ public class StorageAsyncLoader
|
|||||||
public long musicAppsSize;
|
public long musicAppsSize;
|
||||||
public long videoAppsSize;
|
public long videoAppsSize;
|
||||||
public long otherAppsSize;
|
public long otherAppsSize;
|
||||||
public long systemSize;
|
|
||||||
public StorageStatsSource.ExternalStorageStats externalStats;
|
public StorageStatsSource.ExternalStorageStats externalStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -23,6 +23,7 @@ import android.content.Intent;
|
|||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.net.TrafficStats;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.UserHandle;
|
import android.os.UserHandle;
|
||||||
import android.os.storage.VolumeInfo;
|
import android.os.storage.VolumeInfo;
|
||||||
@@ -30,6 +31,7 @@ import android.support.annotation.VisibleForTesting;
|
|||||||
import android.support.v7.preference.Preference;
|
import android.support.v7.preference.Preference;
|
||||||
import android.support.v7.preference.PreferenceScreen;
|
import android.support.v7.preference.PreferenceScreen;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.util.SparseArray;
|
||||||
|
|
||||||
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
|
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
|
||||||
import com.android.settings.R;
|
import com.android.settings.R;
|
||||||
@@ -237,7 +239,10 @@ public class StorageItemPreferenceController extends PreferenceController {
|
|||||||
setFilesPreferenceVisibility();
|
setFilesPreferenceVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onLoadFinished(StorageAsyncLoader.AppsStorageResult data) {
|
public void onLoadFinished(SparseArray<StorageAsyncLoader.AppsStorageResult> result,
|
||||||
|
int userId) {
|
||||||
|
final StorageAsyncLoader.AppsStorageResult data = result.get(userId);
|
||||||
|
|
||||||
// TODO(b/35927909): Figure out how to split out apps which are only installed for work
|
// TODO(b/35927909): Figure out how to split out apps which are only installed for work
|
||||||
// profiles in order to attribute those app's code bytes only to that profile.
|
// profiles in order to attribute those app's code bytes only to that profile.
|
||||||
mPhotoPreference.setStorageSize(
|
mPhotoPreference.setStorageSize(
|
||||||
@@ -248,23 +253,30 @@ public class StorageItemPreferenceController extends PreferenceController {
|
|||||||
mMoviesPreference.setStorageSize(data.videoAppsSize, mTotalSize);
|
mMoviesPreference.setStorageSize(data.videoAppsSize, mTotalSize);
|
||||||
mAppPreference.setStorageSize(data.otherAppsSize, mTotalSize);
|
mAppPreference.setStorageSize(data.otherAppsSize, mTotalSize);
|
||||||
|
|
||||||
long unattributedExternalBytes =
|
long otherExternalBytes =
|
||||||
data.externalStats.totalBytes
|
data.externalStats.totalBytes
|
||||||
- data.externalStats.audioBytes
|
- data.externalStats.audioBytes
|
||||||
- data.externalStats.videoBytes
|
- data.externalStats.videoBytes
|
||||||
- data.externalStats.imageBytes;
|
- data.externalStats.imageBytes
|
||||||
mFilePreference.setStorageSize(unattributedExternalBytes, mTotalSize);
|
- data.externalStats.appBytes;
|
||||||
|
mFilePreference.setStorageSize(otherExternalBytes, mTotalSize);
|
||||||
|
|
||||||
// We define the system size as everything we can't classify.
|
|
||||||
if (mSystemPreference != null) {
|
if (mSystemPreference != null) {
|
||||||
mSystemPreference.setStorageSize(
|
// Everything else that hasn't already been attributed is tracked as
|
||||||
mUsedBytes
|
// belonging to system.
|
||||||
- data.externalStats.totalBytes
|
long attributedSize = 0;
|
||||||
- data.musicAppsSize
|
for (int i = 0; i < result.size(); i++) {
|
||||||
- data.gamesSize
|
final StorageAsyncLoader.AppsStorageResult otherData = result.valueAt(i);
|
||||||
- data.videoAppsSize
|
attributedSize += otherData.gamesSize
|
||||||
- data.otherAppsSize,
|
+ otherData.musicAppsSize
|
||||||
mTotalSize);
|
+ otherData.videoAppsSize
|
||||||
|
+ otherData.otherAppsSize;
|
||||||
|
attributedSize += otherData.externalStats.totalBytes
|
||||||
|
- otherData.externalStats.appBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
final long systemSize = Math.max(TrafficStats.GB_IN_BYTES, mUsedBytes - attributedSize);
|
||||||
|
mSystemPreference.setStorageSize(systemSize, mTotalSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -83,7 +83,7 @@ public class MusicViewHolderControllerTest {
|
|||||||
@Test
|
@Test
|
||||||
public void storageShouldRepresentStorageStatsQuery() throws Exception {
|
public void storageShouldRepresentStorageStatsQuery() throws Exception {
|
||||||
when(mSource.getExternalStorageStats(nullable(String.class), nullable(UserHandle.class))).thenReturn(
|
when(mSource.getExternalStorageStats(nullable(String.class), nullable(UserHandle.class))).thenReturn(
|
||||||
new StorageStatsSource.ExternalStorageStats(1, 1, 0, 0));
|
new StorageStatsSource.ExternalStorageStats(1, 1, 0, 0, 0));
|
||||||
|
|
||||||
mController.queryStats();
|
mController.queryStats();
|
||||||
mController.setupView(mHolder);
|
mController.setupView(mHolder);
|
||||||
|
@@ -17,6 +17,7 @@ package com.android.settings.deviceinfo;
|
|||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
|
import static org.mockito.Matchers.anyInt;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
@@ -31,11 +32,16 @@ import com.android.settingslib.applications.StorageStatsSource;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Captor;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
import org.robolectric.annotation.Config;
|
import org.robolectric.annotation.Config;
|
||||||
|
|
||||||
@RunWith(SettingsRobolectricTestRunner.class)
|
@RunWith(SettingsRobolectricTestRunner.class)
|
||||||
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
|
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
|
||||||
public class StorageProfileFragmentTest {
|
public class StorageProfileFragmentTest {
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<SparseArray<StorageAsyncLoader.AppsStorageResult>> mCaptor;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void verifyAppSizesAreZeroedOut() {
|
public void verifyAppSizesAreZeroedOut() {
|
||||||
StorageItemPreferenceController controller = mock(StorageItemPreferenceController.class);
|
StorageItemPreferenceController controller = mock(StorageItemPreferenceController.class);
|
||||||
@@ -45,18 +51,17 @@ public class StorageProfileFragmentTest {
|
|||||||
result.otherAppsSize = 200;
|
result.otherAppsSize = 200;
|
||||||
result.gamesSize = 300;
|
result.gamesSize = 300;
|
||||||
result.videoAppsSize = 400;
|
result.videoAppsSize = 400;
|
||||||
result.externalStats = new StorageStatsSource.ExternalStorageStats(6, 1, 2, 3);
|
result.externalStats = new StorageStatsSource.ExternalStorageStats(6, 1, 2, 3, 0);
|
||||||
SparseArray<StorageAsyncLoader.AppsStorageResult> resultsArray = new SparseArray<>();
|
SparseArray<StorageAsyncLoader.AppsStorageResult> resultsArray = new SparseArray<>();
|
||||||
resultsArray.put(0, result);
|
resultsArray.put(0, result);
|
||||||
fragment.setPreferenceController(controller);
|
fragment.setPreferenceController(controller);
|
||||||
|
|
||||||
fragment.onLoadFinished(null, resultsArray);
|
fragment.onLoadFinished(null, resultsArray);
|
||||||
|
|
||||||
ArgumentCaptor<StorageAsyncLoader.AppsStorageResult> resultCaptor = ArgumentCaptor.forClass(
|
MockitoAnnotations.initMocks(this);
|
||||||
StorageAsyncLoader.AppsStorageResult.class);
|
verify(controller).onLoadFinished(mCaptor.capture(), anyInt());
|
||||||
verify(controller).onLoadFinished(resultCaptor.capture());
|
|
||||||
|
|
||||||
StorageAsyncLoader.AppsStorageResult extractedResult = resultCaptor.getValue();
|
StorageAsyncLoader.AppsStorageResult extractedResult = mCaptor.getValue().get(0);
|
||||||
assertThat(extractedResult.musicAppsSize).isEqualTo(0);
|
assertThat(extractedResult.musicAppsSize).isEqualTo(0);
|
||||||
assertThat(extractedResult.videoAppsSize).isEqualTo(0);
|
assertThat(extractedResult.videoAppsSize).isEqualTo(0);
|
||||||
assertThat(extractedResult.otherAppsSize).isEqualTo(0);
|
assertThat(extractedResult.otherAppsSize).isEqualTo(0);
|
||||||
|
@@ -168,7 +168,7 @@ public class SecondaryUserControllerTest {
|
|||||||
MEGABYTE_IN_BYTES * 30,
|
MEGABYTE_IN_BYTES * 30,
|
||||||
MEGABYTE_IN_BYTES * 10,
|
MEGABYTE_IN_BYTES * 10,
|
||||||
MEGABYTE_IN_BYTES * 10,
|
MEGABYTE_IN_BYTES * 10,
|
||||||
MEGABYTE_IN_BYTES * 10);
|
MEGABYTE_IN_BYTES * 10, 0);
|
||||||
result.put(10, userResult);
|
result.put(10, userResult);
|
||||||
|
|
||||||
mController.handleResult(result);
|
mController.handleResult(result);
|
||||||
|
@@ -36,6 +36,7 @@ import android.graphics.drawable.Drawable;
|
|||||||
import android.os.UserHandle;
|
import android.os.UserHandle;
|
||||||
import android.os.storage.VolumeInfo;
|
import android.os.storage.VolumeInfo;
|
||||||
import android.support.v7.preference.PreferenceScreen;
|
import android.support.v7.preference.PreferenceScreen;
|
||||||
|
import android.util.SparseArray;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
@@ -275,22 +276,22 @@ public class StorageItemPreferenceControllerTest {
|
|||||||
result.videoAppsSize = MEGABYTE_IN_BYTES * 160;
|
result.videoAppsSize = MEGABYTE_IN_BYTES * 160;
|
||||||
result.musicAppsSize = MEGABYTE_IN_BYTES * 40;
|
result.musicAppsSize = MEGABYTE_IN_BYTES * 40;
|
||||||
result.otherAppsSize = MEGABYTE_IN_BYTES * 90;
|
result.otherAppsSize = MEGABYTE_IN_BYTES * 90;
|
||||||
result.systemSize = MEGABYTE_IN_BYTES * 100; // This value is ignored and overridden now.
|
|
||||||
result.externalStats =
|
result.externalStats =
|
||||||
new StorageStatsSource.ExternalStorageStats(
|
new StorageStatsSource.ExternalStorageStats(
|
||||||
MEGABYTE_IN_BYTES * 500, // total
|
MEGABYTE_IN_BYTES * 500, // total
|
||||||
MEGABYTE_IN_BYTES * 100, // audio
|
MEGABYTE_IN_BYTES * 100, // audio
|
||||||
MEGABYTE_IN_BYTES * 150, // video
|
MEGABYTE_IN_BYTES * 150, // video
|
||||||
MEGABYTE_IN_BYTES * 200); // image
|
MEGABYTE_IN_BYTES * 200, 0); // image
|
||||||
|
|
||||||
mController.onLoadFinished(result);
|
SparseArray<StorageAsyncLoader.AppsStorageResult> results = new SparseArray<>();
|
||||||
|
results.put(0, result);
|
||||||
|
mController.onLoadFinished(results, 0);
|
||||||
|
|
||||||
assertThat(audio.getSummary().toString()).isEqualTo("0.14GB");
|
assertThat(audio.getSummary().toString()).isEqualTo("0.14GB");
|
||||||
assertThat(image.getSummary().toString()).isEqualTo("0.35GB");
|
assertThat(image.getSummary().toString()).isEqualTo("0.35GB");
|
||||||
assertThat(games.getSummary().toString()).isEqualTo("0.08GB");
|
assertThat(games.getSummary().toString()).isEqualTo("0.08GB");
|
||||||
assertThat(movies.getSummary().toString()).isEqualTo("0.16GB");
|
assertThat(movies.getSummary().toString()).isEqualTo("0.16GB");
|
||||||
assertThat(apps.getSummary().toString()).isEqualTo("0.09GB");
|
assertThat(apps.getSummary().toString()).isEqualTo("0.09GB");
|
||||||
assertThat(system.getSummary().toString()).isEqualTo("0.10GB");
|
|
||||||
assertThat(files.getSummary().toString()).isEqualTo("0.05GB");
|
assertThat(files.getSummary().toString()).isEqualTo("0.05GB");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -114,7 +114,7 @@ public class UserProfileControllerTest {
|
|||||||
99 * MEGABYTE_IN_BYTES,
|
99 * MEGABYTE_IN_BYTES,
|
||||||
33 * MEGABYTE_IN_BYTES,
|
33 * MEGABYTE_IN_BYTES,
|
||||||
33 * MEGABYTE_IN_BYTES,
|
33 * MEGABYTE_IN_BYTES,
|
||||||
33 * MEGABYTE_IN_BYTES);
|
33 * MEGABYTE_IN_BYTES, 0);
|
||||||
result.put(10, userResult);
|
result.put(10, userResult);
|
||||||
|
|
||||||
mController.handleResult(result);
|
mController.handleResult(result);
|
||||||
|
@@ -133,9 +133,9 @@ public class StorageAsyncLoaderTest {
|
|||||||
info.id = SECONDARY_USER_ID;
|
info.id = SECONDARY_USER_ID;
|
||||||
mUsers.add(info);
|
mUsers.add(info);
|
||||||
when(mSource.getExternalStorageStats(anyString(), eq(UserHandle.SYSTEM)))
|
when(mSource.getExternalStorageStats(anyString(), eq(UserHandle.SYSTEM)))
|
||||||
.thenReturn(new StorageStatsSource.ExternalStorageStats(9, 2, 3, 4));
|
.thenReturn(new StorageStatsSource.ExternalStorageStats(9, 2, 3, 4, 0));
|
||||||
when(mSource.getExternalStorageStats(anyString(), eq(new UserHandle(SECONDARY_USER_ID))))
|
when(mSource.getExternalStorageStats(anyString(), eq(new UserHandle(SECONDARY_USER_ID))))
|
||||||
.thenReturn(new StorageStatsSource.ExternalStorageStats(10, 3, 3, 4));
|
.thenReturn(new StorageStatsSource.ExternalStorageStats(10, 3, 3, 4, 0));
|
||||||
|
|
||||||
SparseArray<StorageAsyncLoader.AppsStorageResult> result = mLoader.loadInBackground();
|
SparseArray<StorageAsyncLoader.AppsStorageResult> result = mLoader.loadInBackground();
|
||||||
|
|
||||||
@@ -144,19 +144,6 @@ public class StorageAsyncLoaderTest {
|
|||||||
assertThat(result.get(SECONDARY_USER_ID).externalStats.totalBytes).isEqualTo(10L);
|
assertThat(result.get(SECONDARY_USER_ID).externalStats.totalBytes).isEqualTo(10L);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testSystemAppsBaseSizeIsAddedToSystem() throws Exception {
|
|
||||||
ApplicationInfo systemApp =
|
|
||||||
addPackage(PACKAGE_NAME_1, 100, 1, 10, ApplicationInfo.CATEGORY_UNDEFINED);
|
|
||||||
systemApp.flags = ApplicationInfo.FLAG_SYSTEM;
|
|
||||||
|
|
||||||
SparseArray<StorageAsyncLoader.AppsStorageResult> result = mLoader.loadInBackground();
|
|
||||||
|
|
||||||
assertThat(result.size()).isEqualTo(1);
|
|
||||||
assertThat(result.get(PRIMARY_USER_ID).otherAppsSize).isEqualTo(10L);
|
|
||||||
assertThat(result.get(PRIMARY_USER_ID).systemSize).isEqualTo(1L);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testUpdatedSystemAppCodeSizeIsCounted() throws Exception {
|
public void testUpdatedSystemAppCodeSizeIsCounted() throws Exception {
|
||||||
ApplicationInfo systemApp =
|
ApplicationInfo systemApp =
|
||||||
|
Reference in New Issue
Block a user