Merge "Add recommendation setting and remove app settings" into sc-dev am: 8beb8407ec am: 259f4ccc12
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/14722119 Change-Id: Id36b4b65657e933dfe26b3a23f64139d2da4c71d
This commit is contained in:
@@ -9850,12 +9850,10 @@ public final class Settings {
|
|||||||
public static final String MEDIA_CONTROLS_RESUME = "qs_media_resumption";
|
public static final String MEDIA_CONTROLS_RESUME = "qs_media_resumption";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controls which packages are blocked from persisting in media controls when resumption is
|
* Controls whether contextual suggestions can be shown in the media controls.
|
||||||
* enabled. The list of packages is set by the user in the Settings app.
|
|
||||||
* @see Settings.Secure#MEDIA_CONTROLS_RESUME
|
|
||||||
* @hide
|
* @hide
|
||||||
*/
|
*/
|
||||||
public static final String MEDIA_CONTROLS_RESUME_BLOCKED = "qs_media_resumption_blocked";
|
public static final String MEDIA_CONTROLS_RECOMMENDATION = "qs_media_recommend";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controls magnification mode when magnification is enabled via a system-wide triple tap
|
* Controls magnification mode when magnification is enabled via a system-wide triple tap
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ public class SecureSettings {
|
|||||||
Settings.Secure.AWARE_TAP_PAUSE_TOUCH_COUNT,
|
Settings.Secure.AWARE_TAP_PAUSE_TOUCH_COUNT,
|
||||||
Settings.Secure.PEOPLE_STRIP,
|
Settings.Secure.PEOPLE_STRIP,
|
||||||
Settings.Secure.MEDIA_CONTROLS_RESUME,
|
Settings.Secure.MEDIA_CONTROLS_RESUME,
|
||||||
Settings.Secure.MEDIA_CONTROLS_RESUME_BLOCKED,
|
Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
|
||||||
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE,
|
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE,
|
||||||
Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
|
Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
|
||||||
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY,
|
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY,
|
||||||
|
|||||||
@@ -251,8 +251,7 @@ public class SecureSettingsValidators {
|
|||||||
VALIDATORS.put(Secure.TAP_GESTURE, BOOLEAN_VALIDATOR);
|
VALIDATORS.put(Secure.TAP_GESTURE, BOOLEAN_VALIDATOR);
|
||||||
VALIDATORS.put(Secure.PEOPLE_STRIP, BOOLEAN_VALIDATOR);
|
VALIDATORS.put(Secure.PEOPLE_STRIP, BOOLEAN_VALIDATOR);
|
||||||
VALIDATORS.put(Secure.MEDIA_CONTROLS_RESUME, BOOLEAN_VALIDATOR);
|
VALIDATORS.put(Secure.MEDIA_CONTROLS_RESUME, BOOLEAN_VALIDATOR);
|
||||||
VALIDATORS.put(Secure.MEDIA_CONTROLS_RESUME_BLOCKED,
|
VALIDATORS.put(Secure.MEDIA_CONTROLS_RECOMMENDATION, BOOLEAN_VALIDATOR);
|
||||||
COLON_SEPARATED_PACKAGE_LIST_VALIDATOR);
|
|
||||||
VALIDATORS.put(Secure.ACCESSIBILITY_MAGNIFICATION_MODE,
|
VALIDATORS.put(Secure.ACCESSIBILITY_MAGNIFICATION_MODE,
|
||||||
new InclusiveIntegerRangeValidator(
|
new InclusiveIntegerRangeValidator(
|
||||||
Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN,
|
Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN,
|
||||||
|
|||||||
@@ -138,17 +138,6 @@ class MediaDataManager(
|
|||||||
private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
|
private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
|
||||||
// There should ONLY be at most one Smartspace media recommendation.
|
// There should ONLY be at most one Smartspace media recommendation.
|
||||||
private var smartspaceMediaTarget: SmartspaceTarget? = null
|
private var smartspaceMediaTarget: SmartspaceTarget? = null
|
||||||
internal var appsBlockedFromResume: MutableSet<String> = Utils.getBlockedMediaApps(context)
|
|
||||||
set(value) {
|
|
||||||
// Update list
|
|
||||||
appsBlockedFromResume.clear()
|
|
||||||
appsBlockedFromResume.addAll(value)
|
|
||||||
|
|
||||||
// Remove any existing resume players that are now blocked
|
|
||||||
appsBlockedFromResume.forEach {
|
|
||||||
removeAllForPackage(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private var smartspaceSession: SmartspaceSession? = null
|
private var smartspaceSession: SmartspaceSession? = null
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@@ -690,7 +679,9 @@ class MediaDataManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSmartspaceTargetsUpdated(targets: List<Parcelable>) {
|
override fun onSmartspaceTargetsUpdated(targets: List<Parcelable>) {
|
||||||
Log.d(TAG, "My Smartspace media updates are here")
|
if (!Utils.allowMediaRecommendations(context)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
val mediaTargets = targets.filterIsInstance<SmartspaceTarget>()
|
val mediaTargets = targets.filterIsInstance<SmartspaceTarget>()
|
||||||
when (mediaTargets.size) {
|
when (mediaTargets.size) {
|
||||||
0 -> {
|
0 -> {
|
||||||
@@ -736,8 +727,7 @@ class MediaDataManager(
|
|||||||
fun onNotificationRemoved(key: String) {
|
fun onNotificationRemoved(key: String) {
|
||||||
Assert.isMainThread()
|
Assert.isMainThread()
|
||||||
val removed = mediaEntries.remove(key)
|
val removed = mediaEntries.remove(key)
|
||||||
if (useMediaResumption && removed?.resumeAction != null &&
|
if (useMediaResumption && removed?.resumeAction != null && removed?.isLocalSession) {
|
||||||
!isBlockedFromResume(removed.packageName) && removed?.isLocalSession == true) {
|
|
||||||
Log.d(TAG, "Not removing $key because resumable")
|
Log.d(TAG, "Not removing $key because resumable")
|
||||||
// Move to resume key (aka package name) if that key doesn't already exist.
|
// Move to resume key (aka package name) if that key doesn't already exist.
|
||||||
val resumeAction = getResumeMediaAction(removed.resumeAction!!)
|
val resumeAction = getResumeMediaAction(removed.resumeAction!!)
|
||||||
@@ -765,13 +755,6 @@ class MediaDataManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isBlockedFromResume(packageName: String?): Boolean {
|
|
||||||
if (packageName == null) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return appsBlockedFromResume.contains(packageName)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setMediaResumptionEnabled(isEnabled: Boolean) {
|
fun setMediaResumptionEnabled(isEnabled: Boolean) {
|
||||||
if (useMediaResumption == isEnabled) {
|
if (useMediaResumption == isEnabled) {
|
||||||
return
|
return
|
||||||
@@ -844,7 +827,6 @@ class MediaDataManager(
|
|||||||
println("externalListeners: ${mediaDataFilter.listeners}")
|
println("externalListeners: ${mediaDataFilter.listeners}")
|
||||||
println("mediaEntries: $mediaEntries")
|
println("mediaEntries: $mediaEntries")
|
||||||
println("useMediaResumption: $useMediaResumption")
|
println("useMediaResumption: $useMediaResumption")
|
||||||
println("appsBlockedFromResume: $appsBlockedFromResume")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ class MediaResumeListener @Inject constructor(
|
|||||||
|
|
||||||
private var useMediaResumption: Boolean = Utils.useMediaResumption(context)
|
private var useMediaResumption: Boolean = Utils.useMediaResumption(context)
|
||||||
private val resumeComponents: ConcurrentLinkedQueue<ComponentName> = ConcurrentLinkedQueue()
|
private val resumeComponents: ConcurrentLinkedQueue<ComponentName> = ConcurrentLinkedQueue()
|
||||||
private var blockedApps: MutableSet<String> = Utils.getBlockedMediaApps(context)
|
|
||||||
|
|
||||||
private lateinit var mediaDataManager: MediaDataManager
|
private lateinit var mediaDataManager: MediaDataManager
|
||||||
|
|
||||||
@@ -123,14 +122,6 @@ class MediaResumeListener @Inject constructor(
|
|||||||
mediaDataManager.setMediaResumptionEnabled(useMediaResumption)
|
mediaDataManager.setMediaResumptionEnabled(useMediaResumption)
|
||||||
}
|
}
|
||||||
}, Settings.Secure.MEDIA_CONTROLS_RESUME)
|
}, Settings.Secure.MEDIA_CONTROLS_RESUME)
|
||||||
|
|
||||||
// Listen to changes in which apps are allowed to persist
|
|
||||||
tunerService.addTunable(object : TunerService.Tunable {
|
|
||||||
override fun onTuningChanged(key: String?, newValue: String?) {
|
|
||||||
blockedApps = Utils.getBlockedMediaApps(context)
|
|
||||||
mediaDataManager.appsBlockedFromResume = blockedApps
|
|
||||||
}
|
|
||||||
}, Settings.Secure.MEDIA_CONTROLS_RESUME_BLOCKED)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadSavedComponents() {
|
private fun loadSavedComponents() {
|
||||||
@@ -159,10 +150,8 @@ class MediaResumeListener @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
resumeComponents.forEach {
|
resumeComponents.forEach {
|
||||||
if (!blockedApps.contains(it.packageName)) {
|
val browser = mediaBrowserFactory.create(mediaBrowserCallback, it)
|
||||||
val browser = mediaBrowserFactory.create(mediaBrowserCallback, it)
|
browser.findRecentMedia()
|
||||||
browser.findRecentMedia()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,8 +161,7 @@ class MediaResumeListener @Inject constructor(
|
|||||||
mediaBrowser?.disconnect()
|
mediaBrowser?.disconnect()
|
||||||
mediaBrowser = null
|
mediaBrowser = null
|
||||||
// If we don't have a resume action, check if we haven't already
|
// If we don't have a resume action, check if we haven't already
|
||||||
if (data.resumeAction == null && !data.hasCheckedForResume &&
|
if (data.resumeAction == null && !data.hasCheckedForResume && data.isLocalSession) {
|
||||||
!blockedApps.contains(data.packageName) && data.isLocalSession) {
|
|
||||||
// TODO also check for a media button receiver intended for restarting (b/154127084)
|
// TODO also check for a media button receiver intended for restarting (b/154127084)
|
||||||
Log.d(TAG, "Checking for service component for " + data.packageName)
|
Log.d(TAG, "Checking for service component for " + data.packageName)
|
||||||
val pm = context.packageManager
|
val pm = context.packageManager
|
||||||
|
|||||||
@@ -68,8 +68,7 @@ public class TunerServiceImpl extends TunerService {
|
|||||||
private static final String[] RESET_EXCEPTION_LIST = new String[] {
|
private static final String[] RESET_EXCEPTION_LIST = new String[] {
|
||||||
QSTileHost.TILES_SETTING,
|
QSTileHost.TILES_SETTING,
|
||||||
Settings.Secure.DOZE_ALWAYS_ON,
|
Settings.Secure.DOZE_ALWAYS_ON,
|
||||||
Settings.Secure.MEDIA_CONTROLS_RESUME,
|
Settings.Secure.MEDIA_CONTROLS_RESUME
|
||||||
Secure.MEDIA_CONTROLS_RESUME_BLOCKED
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private final Observer mObserver = new Observer();
|
private final Observer mObserver = new Observer();
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import android.content.pm.PackageManager;
|
|||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.ContextThemeWrapper;
|
import android.view.ContextThemeWrapper;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
@@ -32,9 +31,7 @@ import com.android.systemui.shared.system.QuickStepContract;
|
|||||||
import com.android.systemui.statusbar.CommandQueue;
|
import com.android.systemui.statusbar.CommandQueue;
|
||||||
import com.android.systemui.statusbar.FeatureFlags;
|
import com.android.systemui.statusbar.FeatureFlags;
|
||||||
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
public class Utils {
|
public class Utils {
|
||||||
@@ -144,7 +141,7 @@ public class Utils {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Allow media resumption controls. Requires {@link #useQsMediaPlayer(Context)} to be enabled.
|
* Allow media resumption controls. Requires {@link #useQsMediaPlayer(Context)} to be enabled.
|
||||||
* Off by default, but can be enabled by setting to 1
|
* On by default, but can be disabled by setting to 0
|
||||||
*/
|
*/
|
||||||
public static boolean useMediaResumption(Context context) {
|
public static boolean useMediaResumption(Context context) {
|
||||||
int flag = Settings.Secure.getInt(context.getContentResolver(),
|
int flag = Settings.Secure.getInt(context.getContentResolver(),
|
||||||
@@ -153,20 +150,14 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the set of apps for which the user has manually disabled resumption.
|
* Allow recommendations from smartspace to show in media controls.
|
||||||
|
* Requires {@link #useQsMediaPlayer(Context)} to be enabled.
|
||||||
|
* On by default, but can be disabled by setting to 0
|
||||||
*/
|
*/
|
||||||
public static Set<String> getBlockedMediaApps(Context context) {
|
public static boolean allowMediaRecommendations(Context context) {
|
||||||
String list = Settings.Secure.getString(context.getContentResolver(),
|
int flag = Settings.Secure.getInt(context.getContentResolver(),
|
||||||
Settings.Secure.MEDIA_CONTROLS_RESUME_BLOCKED);
|
Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 1);
|
||||||
if (TextUtils.isEmpty(list)) {
|
return useQsMediaPlayer(context) && flag > 0;
|
||||||
return new HashSet<>();
|
|
||||||
}
|
|
||||||
String[] names = list.split(":");
|
|
||||||
Set<String> apps = new HashSet<>(names.length);
|
|
||||||
for (String s : names) {
|
|
||||||
apps.add(s);
|
|
||||||
}
|
|
||||||
return apps;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.media.MediaDescription
|
|||||||
import android.media.MediaMetadata
|
import android.media.MediaMetadata
|
||||||
import android.media.session.MediaController
|
import android.media.session.MediaController
|
||||||
import android.media.session.MediaSession
|
import android.media.session.MediaSession
|
||||||
|
import android.provider.Settings
|
||||||
import android.service.notification.StatusBarNotification
|
import android.service.notification.StatusBarNotification
|
||||||
import android.testing.AndroidTestingRunner
|
import android.testing.AndroidTestingRunner
|
||||||
import android.testing.TestableLooper.RunWithLooper
|
import android.testing.TestableLooper.RunWithLooper
|
||||||
@@ -83,11 +84,16 @@ class MediaDataManagerTest : SysuiTestCase() {
|
|||||||
@Captor lateinit var mediaDataCaptor: ArgumentCaptor<MediaData>
|
@Captor lateinit var mediaDataCaptor: ArgumentCaptor<MediaData>
|
||||||
private val clock = FakeSystemClock()
|
private val clock = FakeSystemClock()
|
||||||
|
|
||||||
|
private val originalSmartspaceSetting = Settings.Secure.getInt(context.contentResolver,
|
||||||
|
Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 1)
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
foregroundExecutor = FakeExecutor(clock)
|
foregroundExecutor = FakeExecutor(clock)
|
||||||
backgroundExecutor = FakeExecutor(clock)
|
backgroundExecutor = FakeExecutor(clock)
|
||||||
smartspaceMediaDataProvider = SmartspaceMediaDataProvider()
|
smartspaceMediaDataProvider = SmartspaceMediaDataProvider()
|
||||||
|
Settings.Secure.putInt(context.contentResolver,
|
||||||
|
Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 1)
|
||||||
mediaDataManager = MediaDataManager(
|
mediaDataManager = MediaDataManager(
|
||||||
context = context,
|
context = context,
|
||||||
backgroundExecutor = backgroundExecutor,
|
backgroundExecutor = backgroundExecutor,
|
||||||
@@ -139,6 +145,8 @@ class MediaDataManagerTest : SysuiTestCase() {
|
|||||||
fun tearDown() {
|
fun tearDown() {
|
||||||
session.release()
|
session.release()
|
||||||
mediaDataManager.destroy()
|
mediaDataManager.destroy()
|
||||||
|
Settings.Secure.putInt(context.contentResolver,
|
||||||
|
Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, originalSmartspaceSetting)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -256,55 +264,6 @@ class MediaDataManagerTest : SysuiTestCase() {
|
|||||||
verify(listener).onMediaDataRemoved(eq(KEY))
|
verify(listener).onMediaDataRemoved(eq(KEY))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testAppBlockedFromResumption() {
|
|
||||||
// GIVEN that the manager has a notification with a resume action
|
|
||||||
whenever(controller.metadata).thenReturn(metadataBuilder.build())
|
|
||||||
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
|
|
||||||
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
|
|
||||||
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
|
|
||||||
verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor))
|
|
||||||
val data = mediaDataCaptor.value
|
|
||||||
assertThat(data.resumption).isFalse()
|
|
||||||
mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
|
|
||||||
|
|
||||||
// and the manager should block the package from creating resume controls
|
|
||||||
val blocked = mutableSetOf(PACKAGE_NAME, "com.example.app")
|
|
||||||
mediaDataManager.appsBlockedFromResume = blocked
|
|
||||||
|
|
||||||
// WHEN the notification is removed
|
|
||||||
mediaDataManager.onNotificationRemoved(KEY)
|
|
||||||
|
|
||||||
// THEN the media data is removed
|
|
||||||
verify(listener).onMediaDataRemoved(eq(KEY))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testAppUnblockedFromResumption() {
|
|
||||||
// GIVEN that an app was blocked from resuming
|
|
||||||
val blocked = mutableSetOf(PACKAGE_NAME, "com.example.app")
|
|
||||||
mediaDataManager.appsBlockedFromResume = blocked
|
|
||||||
|
|
||||||
// and GIVEN that the manager has a notification from that app with a resume action
|
|
||||||
whenever(controller.metadata).thenReturn(metadataBuilder.build())
|
|
||||||
mediaDataManager.onNotificationAdded(KEY, mediaNotification)
|
|
||||||
assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
|
|
||||||
assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
|
|
||||||
verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor))
|
|
||||||
val data = mediaDataCaptor.value
|
|
||||||
assertThat(data.resumption).isFalse()
|
|
||||||
mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
|
|
||||||
|
|
||||||
// WHEN the app is unblocked
|
|
||||||
mediaDataManager.appsBlockedFromResume = mutableSetOf("com.example.app")
|
|
||||||
|
|
||||||
// and the notification is removed
|
|
||||||
mediaDataManager.onNotificationRemoved(KEY)
|
|
||||||
|
|
||||||
// THEN the entry will stay as a resume control
|
|
||||||
verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testAddResumptionControls() {
|
fun testAddResumptionControls() {
|
||||||
// WHEN resumption controls are added
|
// WHEN resumption controls are added
|
||||||
@@ -381,6 +340,18 @@ class MediaDataManagerTest : SysuiTestCase() {
|
|||||||
verify(listener).onSmartspaceMediaDataRemoved(KEY_MEDIA_SMARTSPACE)
|
verify(listener).onSmartspaceMediaDataRemoved(KEY_MEDIA_SMARTSPACE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testOnSmartspaceMediaDataLoaded_settingDisabled_doesNothing() {
|
||||||
|
// WHEN media recommendation setting is off
|
||||||
|
Settings.Secure.putInt(context.contentResolver,
|
||||||
|
Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 0)
|
||||||
|
smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
|
||||||
|
|
||||||
|
// THEN smartspace signal is ignored
|
||||||
|
verify(listener, never())
|
||||||
|
.onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean())
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testOnMediaDataChanged_updatesLastActiveTime() {
|
fun testOnMediaDataChanged_updatesLastActiveTime() {
|
||||||
val currentTime = clock.elapsedRealtime()
|
val currentTime = clock.elapsedRealtime()
|
||||||
|
|||||||
Reference in New Issue
Block a user