Merge "All exact alarms now require SCHEDULE_EXACT_ALARM" into sc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
235fae7fdb
@@ -539,6 +539,11 @@ public class AlarmManager {
|
||||
* scheduled as exact. Applications are strongly discouraged from using exact
|
||||
* alarms unnecessarily as they reduce the OS's ability to minimize battery use.
|
||||
*
|
||||
* <p>
|
||||
* Starting with {@link Build.VERSION_CODES#S}, apps require the
|
||||
* {@link Manifest.permission#SCHEDULE_EXACT_ALARM SCHEDULE_EXACT_ALARM} permission to use this
|
||||
* API.
|
||||
*
|
||||
* @param type type of alarm.
|
||||
* @param triggerAtMillis time in milliseconds that the alarm should go
|
||||
* off, using the appropriate clock (depending on the alarm type).
|
||||
@@ -558,6 +563,7 @@ public class AlarmManager {
|
||||
* @see #RTC
|
||||
* @see #RTC_WAKEUP
|
||||
*/
|
||||
@RequiresPermission(value = Manifest.permission.SCHEDULE_EXACT_ALARM, conditional = true)
|
||||
public void setExact(@AlarmType int type, long triggerAtMillis, PendingIntent operation) {
|
||||
setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, 0, operation, null, null, null,
|
||||
null, null);
|
||||
@@ -571,7 +577,13 @@ public class AlarmManager {
|
||||
* The OnAlarmListener's {@link OnAlarmListener#onAlarm() onAlarm()} method will be
|
||||
* invoked via the specified target Handler, or on the application's main looper
|
||||
* if {@code null} is passed as the {@code targetHandler} parameter.
|
||||
*
|
||||
* <p>
|
||||
* Starting with {@link Build.VERSION_CODES#S}, apps require the
|
||||
* {@link Manifest.permission#SCHEDULE_EXACT_ALARM SCHEDULE_EXACT_ALARM} permission to use this
|
||||
* API.
|
||||
*/
|
||||
@RequiresPermission(value = Manifest.permission.SCHEDULE_EXACT_ALARM, conditional = true)
|
||||
public void setExact(@AlarmType int type, long triggerAtMillis, String tag,
|
||||
OnAlarmListener listener, Handler targetHandler) {
|
||||
setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, 0, null, listener, tag,
|
||||
|
||||
@@ -388,6 +388,8 @@ public class AlarmManagerService extends SystemService {
|
||||
@VisibleForTesting
|
||||
static final String KEY_MAX_INTERVAL = "max_interval";
|
||||
@VisibleForTesting
|
||||
static final String KEY_MIN_WINDOW = "min_window";
|
||||
@VisibleForTesting
|
||||
static final String KEY_ALLOW_WHILE_IDLE_WHITELIST_DURATION
|
||||
= "allow_while_idle_whitelist_duration";
|
||||
@VisibleForTesting
|
||||
@@ -428,11 +430,13 @@ public class AlarmManagerService extends SystemService {
|
||||
@VisibleForTesting
|
||||
static final String KEY_ALLOW_WHILE_IDLE_COMPAT_WINDOW = "allow_while_idle_compat_window";
|
||||
|
||||
private static final String KEY_CRASH_NON_CLOCK_APPS = "crash_non_clock_apps";
|
||||
@VisibleForTesting
|
||||
static final String KEY_CRASH_NON_CLOCK_APPS = "crash_non_clock_apps";
|
||||
|
||||
private static final long DEFAULT_MIN_FUTURITY = 5 * 1000;
|
||||
private static final long DEFAULT_MIN_INTERVAL = 60 * 1000;
|
||||
private static final long DEFAULT_MAX_INTERVAL = 365 * INTERVAL_DAY;
|
||||
private static final long DEFAULT_MIN_WINDOW = 10_000;
|
||||
private static final long DEFAULT_ALLOW_WHILE_IDLE_WHITELIST_DURATION = 10 * 1000;
|
||||
private static final long DEFAULT_LISTENER_TIMEOUT = 5 * 1000;
|
||||
private static final int DEFAULT_MAX_ALARMS_PER_UID = 500;
|
||||
@@ -475,6 +479,9 @@ public class AlarmManagerService extends SystemService {
|
||||
// Maximum alarm recurrence interval
|
||||
public long MAX_INTERVAL = DEFAULT_MAX_INTERVAL;
|
||||
|
||||
// Minimum window size for inexact alarms
|
||||
public long MIN_WINDOW = DEFAULT_MIN_WINDOW;
|
||||
|
||||
// BroadcastOptions.setTemporaryAppWhitelistDuration() to use for FLAG_ALLOW_WHILE_IDLE.
|
||||
public long ALLOW_WHILE_IDLE_WHITELIST_DURATION
|
||||
= DEFAULT_ALLOW_WHILE_IDLE_WHITELIST_DURATION;
|
||||
@@ -575,6 +582,9 @@ public class AlarmManagerService extends SystemService {
|
||||
ALLOW_WHILE_IDLE_QUOTA = 1;
|
||||
}
|
||||
break;
|
||||
case KEY_MIN_WINDOW:
|
||||
MIN_WINDOW = properties.getLong(KEY_MIN_WINDOW, DEFAULT_MIN_WINDOW);
|
||||
break;
|
||||
case KEY_ALLOW_WHILE_IDLE_COMPAT_QUOTA:
|
||||
ALLOW_WHILE_IDLE_COMPAT_QUOTA = properties.getInt(
|
||||
KEY_ALLOW_WHILE_IDLE_COMPAT_QUOTA,
|
||||
@@ -738,6 +748,11 @@ public class AlarmManagerService extends SystemService {
|
||||
TimeUtils.formatDuration(MAX_INTERVAL, pw);
|
||||
pw.println();
|
||||
|
||||
pw.print(KEY_MIN_WINDOW);
|
||||
pw.print("=");
|
||||
TimeUtils.formatDuration(MIN_WINDOW, pw);
|
||||
pw.println();
|
||||
|
||||
pw.print(KEY_LISTENER_TIMEOUT);
|
||||
pw.print("=");
|
||||
TimeUtils.formatDuration(LISTENER_TIMEOUT, pw);
|
||||
@@ -1642,6 +1657,7 @@ public class AlarmManagerService extends SystemService {
|
||||
// Fix this window in place, so that as time approaches we don't collapse it.
|
||||
windowLength = maxElapsed - triggerElapsed;
|
||||
} else {
|
||||
windowLength = Math.max(windowLength, mConstants.MIN_WINDOW);
|
||||
maxElapsed = triggerElapsed + windowLength;
|
||||
}
|
||||
synchronized (mLock) {
|
||||
@@ -1981,8 +1997,10 @@ public class AlarmManagerService extends SystemService {
|
||||
* Returns true if the given uid does not require SCHEDULE_EXACT_ALARM to set exact,
|
||||
* allow-while-idle alarms.
|
||||
*/
|
||||
boolean isExemptFromPermission(int uid) {
|
||||
return (UserHandle.isSameApp(mSystemUiUid, uid) || mLocalDeviceIdleController == null
|
||||
boolean isExemptFromExactAlarmPermission(int uid) {
|
||||
return (UserHandle.isSameApp(mSystemUiUid, uid)
|
||||
|| UserHandle.isCore(uid)
|
||||
|| mLocalDeviceIdleController == null
|
||||
|| mLocalDeviceIdleController.isAppOnWhitelist(UserHandle.getAppId(uid)));
|
||||
}
|
||||
|
||||
@@ -2002,54 +2020,43 @@ public class AlarmManagerService extends SystemService {
|
||||
mAppOps.checkPackage(callingUid, callingPackage);
|
||||
|
||||
final boolean allowWhileIdle = (flags & FLAG_ALLOW_WHILE_IDLE) != 0;
|
||||
final boolean exact = (windowLength == AlarmManager.WINDOW_EXACT);
|
||||
|
||||
// make sure the caller is allowed to use the requested kind of alarm, and also
|
||||
// decide what quota and broadcast options to use.
|
||||
Bundle idleOptions = null;
|
||||
if (alarmClock != null || allowWhileIdle) {
|
||||
// make sure the caller is allowed to use the requested kind of alarm, and also
|
||||
// decide what broadcast options to use.
|
||||
if (exact || allowWhileIdle) {
|
||||
final boolean needsPermission;
|
||||
boolean lowQuota;
|
||||
boolean lowerQuota;
|
||||
if (CompatChanges.isChangeEnabled(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION,
|
||||
callingPackage, UserHandle.getUserHandleForUid(callingUid))) {
|
||||
if (windowLength != AlarmManager.WINDOW_EXACT) {
|
||||
needsPermission = false;
|
||||
lowQuota = true;
|
||||
idleOptions = isExemptFromPermission(callingUid) ? mOptsWithFgs.toBundle()
|
||||
: mOptsWithoutFgs.toBundle();
|
||||
} else if (alarmClock != null) {
|
||||
needsPermission = true;
|
||||
lowQuota = false;
|
||||
idleOptions = mOptsWithFgs.toBundle();
|
||||
} else {
|
||||
needsPermission = true;
|
||||
lowQuota = false;
|
||||
idleOptions = mOptsWithFgs.toBundle();
|
||||
}
|
||||
needsPermission = exact;
|
||||
lowerQuota = !exact;
|
||||
idleOptions = exact ? mOptsWithFgs.toBundle() : mOptsWithoutFgs.toBundle();
|
||||
} else {
|
||||
needsPermission = false;
|
||||
lowQuota = allowWhileIdle;
|
||||
lowerQuota = allowWhileIdle;
|
||||
idleOptions = allowWhileIdle ? mOptsWithFgs.toBundle() : null;
|
||||
}
|
||||
if (needsPermission && !canScheduleExactAlarms()) {
|
||||
if (alarmClock == null && isExemptFromPermission(callingUid)) {
|
||||
// If the app is on the full system allow-list (not except-idle), we still
|
||||
// allow the alarms, but with a lower quota to keep pre-S compatibility.
|
||||
lowQuota = true;
|
||||
} else {
|
||||
if (alarmClock != null || !isExemptFromExactAlarmPermission(callingUid)) {
|
||||
final String errorMessage = "Caller " + callingPackage + " needs to hold "
|
||||
+ Manifest.permission.SCHEDULE_EXACT_ALARM + " to set "
|
||||
+ ((allowWhileIdle) ? "exact, allow-while-idle" : "alarm-clock")
|
||||
+ " alarms.";
|
||||
+ "exact alarms.";
|
||||
if (mConstants.CRASH_NON_CLOCK_APPS) {
|
||||
throw new SecurityException(errorMessage);
|
||||
} else {
|
||||
Slog.wtf(TAG, errorMessage);
|
||||
idleOptions = mOptsWithoutFgs.toBundle();
|
||||
lowQuota = allowWhileIdle;
|
||||
}
|
||||
}
|
||||
// If the app is on the full system power allow-list (not except-idle), or we're
|
||||
// in a soft failure mode, we still allow the alarms.
|
||||
// We give temporary allowlist to allow-while-idle alarms but without FGS
|
||||
// capability. Note that apps that are in the power allow-list do not need it.
|
||||
idleOptions = allowWhileIdle ? mOptsWithoutFgs.toBundle() : null;
|
||||
lowerQuota = allowWhileIdle;
|
||||
}
|
||||
if (lowQuota) {
|
||||
if (lowerQuota) {
|
||||
flags &= ~FLAG_ALLOW_WHILE_IDLE;
|
||||
flags |= FLAG_ALLOW_WHILE_IDLE_COMPAT;
|
||||
}
|
||||
@@ -2998,13 +3005,10 @@ public class AlarmManagerService extends SystemService {
|
||||
/**
|
||||
* Called when an app loses {@link Manifest.permission#SCHEDULE_EXACT_ALARM} to remove alarms
|
||||
* that the app is no longer eligible to use.
|
||||
* TODO (b/179541791): Revisit and write tests once UX is final.
|
||||
* TODO (b/179541791): Add revocation history to dumpsys.
|
||||
*/
|
||||
void removeExactAlarmsOnPermissionRevokedLocked(int uid, String packageName) {
|
||||
if (UserHandle.isCore(uid) || uid == mSystemUiUid) {
|
||||
return;
|
||||
}
|
||||
if (isExemptFromPermission(uid)) {
|
||||
if (isExemptFromExactAlarmPermission(uid)) {
|
||||
return;
|
||||
}
|
||||
if (!CompatChanges.isChangeEnabled(
|
||||
@@ -3015,7 +3019,7 @@ public class AlarmManagerService extends SystemService {
|
||||
|
||||
final Predicate<Alarm> whichAlarms =
|
||||
a -> (a.uid == uid && a.packageName.equals(packageName)
|
||||
&& ((a.flags & FLAG_ALLOW_WHILE_IDLE) != 0 || a.alarmClock != null));
|
||||
&& a.windowLength == AlarmManager.WINDOW_EXACT);
|
||||
final ArrayList<Alarm> removed = mAlarmStore.remove(whichAlarms);
|
||||
final boolean didRemove = !removed.isEmpty();
|
||||
if (didRemove) {
|
||||
@@ -3873,6 +3877,7 @@ public class AlarmManagerService extends SystemService {
|
||||
return alarm.creatorUid;
|
||||
}
|
||||
|
||||
|
||||
@VisibleForTesting
|
||||
class AlarmHandler extends Handler {
|
||||
public static final int ALARM_EVENT = 1;
|
||||
|
||||
@@ -133,6 +133,11 @@ public interface AlarmStore {
|
||||
*/
|
||||
void dumpProto(ProtoOutputStream pos, long nowElapsed);
|
||||
|
||||
/**
|
||||
* @return a name for this alarm store that can be used for debugging and tests.
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* A functional interface used to update the alarm. Used to describe the update in
|
||||
* {@link #updateAlarmDeliveries(AlarmDeliveryCalculator)}
|
||||
|
||||
@@ -27,6 +27,7 @@ import android.util.IndentingPrintWriter;
|
||||
import android.util.Slog;
|
||||
import android.util.proto.ProtoOutputStream;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.util.StatLogger;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
@@ -40,6 +41,8 @@ import java.util.function.Predicate;
|
||||
* This keeps the alarms in batches, which are sorted on the start time of their delivery window.
|
||||
*/
|
||||
public class BatchingAlarmStore implements AlarmStore {
|
||||
@VisibleForTesting
|
||||
static final String TAG = BatchingAlarmStore.class.getSimpleName();
|
||||
|
||||
private final ArrayList<Batch> mAlarmBatches = new ArrayList<>();
|
||||
private int mSize;
|
||||
@@ -49,7 +52,7 @@ public class BatchingAlarmStore implements AlarmStore {
|
||||
int REBATCH_ALL_ALARMS = 0;
|
||||
}
|
||||
|
||||
final StatLogger mStatLogger = new StatLogger("BatchingAlarmStore stats", new String[]{
|
||||
final StatLogger mStatLogger = new StatLogger(TAG + " stats", new String[]{
|
||||
"REBATCH_ALL_ALARMS",
|
||||
});
|
||||
|
||||
@@ -211,6 +214,11 @@ public class BatchingAlarmStore implements AlarmStore {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
private void insertAndBatchAlarm(Alarm alarm) {
|
||||
final int whichBatch = ((alarm.flags & AlarmManager.FLAG_STANDALONE) != 0) ? -1
|
||||
: attemptCoalesce(alarm.getWhenElapsed(), alarm.getMaxWhenElapsed());
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.util.IndentingPrintWriter;
|
||||
import android.util.Slog;
|
||||
import android.util.proto.ProtoOutputStream;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.util.StatLogger;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
@@ -38,6 +39,8 @@ import java.util.function.Predicate;
|
||||
* This keeps the alarms in a sorted list, and only batches them at the time of delivery.
|
||||
*/
|
||||
public class LazyAlarmStore implements AlarmStore {
|
||||
@VisibleForTesting
|
||||
static final String TAG = LazyAlarmStore.class.getSimpleName();
|
||||
|
||||
private final ArrayList<Alarm> mAlarms = new ArrayList<>();
|
||||
private Runnable mOnAlarmClockRemoved;
|
||||
@@ -47,7 +50,7 @@ public class LazyAlarmStore implements AlarmStore {
|
||||
int GET_NEXT_WAKEUP_DELIVERY_TIME = 1;
|
||||
}
|
||||
|
||||
final StatLogger mStatLogger = new StatLogger("LazyAlarmStore stats", new String[]{
|
||||
final StatLogger mStatLogger = new StatLogger(TAG + " stats", new String[]{
|
||||
"GET_NEXT_DELIVERY_TIME",
|
||||
"GET_NEXT_WAKEUP_DELIVERY_TIME",
|
||||
});
|
||||
@@ -214,4 +217,9 @@ public class LazyAlarmStore implements AlarmStore {
|
||||
a.dumpDebug(pos, AlarmManagerServiceDumpProto.PENDING_ALARMS, nowElapsed);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return TAG;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4351,8 +4351,8 @@ package android.app {
|
||||
method public void set(int, long, String, android.app.AlarmManager.OnAlarmListener, android.os.Handler);
|
||||
method @RequiresPermission(android.Manifest.permission.SCHEDULE_EXACT_ALARM) public void setAlarmClock(android.app.AlarmManager.AlarmClockInfo, android.app.PendingIntent);
|
||||
method public void setAndAllowWhileIdle(int, long, android.app.PendingIntent);
|
||||
method public void setExact(int, long, android.app.PendingIntent);
|
||||
method public void setExact(int, long, String, android.app.AlarmManager.OnAlarmListener, android.os.Handler);
|
||||
method @RequiresPermission(value=android.Manifest.permission.SCHEDULE_EXACT_ALARM, conditional=true) public void setExact(int, long, android.app.PendingIntent);
|
||||
method @RequiresPermission(value=android.Manifest.permission.SCHEDULE_EXACT_ALARM, conditional=true) public void setExact(int, long, String, android.app.AlarmManager.OnAlarmListener, android.os.Handler);
|
||||
method @RequiresPermission(value=android.Manifest.permission.SCHEDULE_EXACT_ALARM, conditional=true) public void setExactAndAllowWhileIdle(int, long, android.app.PendingIntent);
|
||||
method public void setInexactRepeating(int, long, long, android.app.PendingIntent);
|
||||
method public void setRepeating(int, long, long, android.app.PendingIntent);
|
||||
|
||||
@@ -27,6 +27,7 @@ import static android.app.AlarmManager.RTC;
|
||||
import static android.app.AlarmManager.RTC_WAKEUP;
|
||||
import static android.app.AlarmManager.WINDOW_EXACT;
|
||||
import static android.app.AlarmManager.WINDOW_HEURISTIC;
|
||||
import static android.app.AppOpsManager.OP_SCHEDULE_EXACT_ALARM;
|
||||
import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_ACTIVE;
|
||||
import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_FREQUENT;
|
||||
import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RARE;
|
||||
@@ -47,17 +48,20 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
|
||||
import static com.android.server.alarm.AlarmManagerService.ACTIVE_INDEX;
|
||||
import static com.android.server.alarm.AlarmManagerService.AlarmHandler.APP_STANDBY_BUCKET_CHANGED;
|
||||
import static com.android.server.alarm.AlarmManagerService.AlarmHandler.CHARGING_STATUS_CHANGED;
|
||||
import static com.android.server.alarm.AlarmManagerService.AlarmHandler.REMOVE_EXACT_ALARMS;
|
||||
import static com.android.server.alarm.AlarmManagerService.AlarmHandler.REMOVE_FOR_CANCELED;
|
||||
import static com.android.server.alarm.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_COMPAT_QUOTA;
|
||||
import static com.android.server.alarm.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_COMPAT_WINDOW;
|
||||
import static com.android.server.alarm.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_QUOTA;
|
||||
import static com.android.server.alarm.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_WHITELIST_DURATION;
|
||||
import static com.android.server.alarm.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_WINDOW;
|
||||
import static com.android.server.alarm.AlarmManagerService.Constants.KEY_CRASH_NON_CLOCK_APPS;
|
||||
import static com.android.server.alarm.AlarmManagerService.Constants.KEY_LAZY_BATCHING;
|
||||
import static com.android.server.alarm.AlarmManagerService.Constants.KEY_LISTENER_TIMEOUT;
|
||||
import static com.android.server.alarm.AlarmManagerService.Constants.KEY_MAX_INTERVAL;
|
||||
import static com.android.server.alarm.AlarmManagerService.Constants.KEY_MIN_FUTURITY;
|
||||
import static com.android.server.alarm.AlarmManagerService.Constants.KEY_MIN_INTERVAL;
|
||||
import static com.android.server.alarm.AlarmManagerService.Constants.KEY_MIN_WINDOW;
|
||||
import static com.android.server.alarm.AlarmManagerService.FREQUENT_INDEX;
|
||||
import static com.android.server.alarm.AlarmManagerService.INDEFINITE_DELAY;
|
||||
import static com.android.server.alarm.AlarmManagerService.IS_WAKEUP_MASK;
|
||||
@@ -71,6 +75,7 @@ import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
@@ -81,6 +86,7 @@ import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.ActivityManager;
|
||||
@@ -408,7 +414,7 @@ public class AlarmManagerServiceTest {
|
||||
ArgumentCaptor<IAppOpsCallback> appOpsCallbackCaptor = ArgumentCaptor.forClass(
|
||||
IAppOpsCallback.class);
|
||||
try {
|
||||
verify(mIAppOpsService).startWatchingMode(eq(AppOpsManager.OP_SCHEDULE_EXACT_ALARM),
|
||||
verify(mIAppOpsService).startWatchingMode(eq(OP_SCHEDULE_EXACT_ALARM),
|
||||
isNull(), appOpsCallbackCaptor.capture());
|
||||
} catch (RemoteException e) {
|
||||
// Not expected on a mock.
|
||||
@@ -445,12 +451,12 @@ public class AlarmManagerServiceTest {
|
||||
|
||||
private void setTestAlarm(int type, long triggerTime, PendingIntent operation, long interval,
|
||||
int flags, int callingUid) {
|
||||
setTestAlarm(type, triggerTime, operation, interval, flags, callingUid, null);
|
||||
setTestAlarm(type, triggerTime, 0, operation, interval, flags, callingUid, null);
|
||||
}
|
||||
|
||||
private void setTestAlarm(int type, long triggerTime, PendingIntent operation, long interval,
|
||||
int flags, int callingUid, Bundle idleOptions) {
|
||||
mService.setImpl(type, triggerTime, WINDOW_EXACT, interval, operation, null, "test", flags,
|
||||
private void setTestAlarm(int type, long triggerTime, long windowLength,
|
||||
PendingIntent operation, long interval, int flags, int callingUid, Bundle idleOptions) {
|
||||
mService.setImpl(type, triggerTime, windowLength, interval, operation, null, "test", flags,
|
||||
null, null, callingUid, TEST_CALLING_PACKAGE, idleOptions);
|
||||
}
|
||||
|
||||
@@ -572,6 +578,7 @@ public class AlarmManagerServiceTest {
|
||||
setDeviceConfigLong(KEY_ALLOW_WHILE_IDLE_COMPAT_WINDOW, 35);
|
||||
setDeviceConfigLong(KEY_ALLOW_WHILE_IDLE_WHITELIST_DURATION, 40);
|
||||
setDeviceConfigLong(KEY_LISTENER_TIMEOUT, 45);
|
||||
setDeviceConfigLong(KEY_MIN_WINDOW, 50);
|
||||
assertEquals(5, mService.mConstants.MIN_FUTURITY);
|
||||
assertEquals(10, mService.mConstants.MIN_INTERVAL);
|
||||
assertEquals(15, mService.mConstants.MAX_INTERVAL);
|
||||
@@ -581,6 +588,7 @@ public class AlarmManagerServiceTest {
|
||||
assertEquals(35, mService.mConstants.ALLOW_WHILE_IDLE_COMPAT_WINDOW);
|
||||
assertEquals(40, mService.mConstants.ALLOW_WHILE_IDLE_WHITELIST_DURATION);
|
||||
assertEquals(45, mService.mConstants.LISTENER_TIMEOUT);
|
||||
assertEquals(50, mService.mConstants.MIN_WINDOW);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1644,6 +1652,10 @@ public class AlarmManagerServiceTest {
|
||||
getNewMockPendingIntent(), null, null, null,
|
||||
mock(AlarmManager.AlarmClockInfo.class));
|
||||
|
||||
// exact
|
||||
mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_EXACT, 0,
|
||||
0, getNewMockPendingIntent(), null, null, null, null);
|
||||
|
||||
// exact, allow-while-idle
|
||||
mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_EXACT, 0,
|
||||
FLAG_ALLOW_WHILE_IDLE, getNewMockPendingIntent(), null, null, null, null);
|
||||
@@ -1657,6 +1669,22 @@ public class AlarmManagerServiceTest {
|
||||
verify(mDeviceIdleInternal, never()).isAppOnWhitelist(anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exactBinderCallWhenChangeDisabled() throws Exception {
|
||||
doReturn(false).when(
|
||||
() -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),
|
||||
anyString(), any(UserHandle.class)));
|
||||
|
||||
final PendingIntent alarmPi = getNewMockPendingIntent();
|
||||
mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_EXACT, 0,
|
||||
0, alarmPi, null, null, null, null);
|
||||
|
||||
verify(mService).setImpl(eq(ELAPSED_REALTIME_WAKEUP), eq(1234L), eq(WINDOW_EXACT), eq(0L),
|
||||
eq(alarmPi), isNull(), isNull(),
|
||||
eq(FLAG_STANDALONE), isNull(), isNull(),
|
||||
eq(Process.myUid()), eq(TEST_CALLING_PACKAGE), isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exactAllowWhileIdleBinderCallWhenChangeDisabled() throws Exception {
|
||||
doReturn(false).when(
|
||||
@@ -1745,6 +1773,86 @@ public class AlarmManagerServiceTest {
|
||||
assertEquals(TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED, type);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void alarmClockBinderCallWithoutPermission() throws RemoteException {
|
||||
setDeviceConfigBoolean(KEY_CRASH_NON_CLOCK_APPS, true);
|
||||
doReturn(true).when(
|
||||
() -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),
|
||||
anyString(), any(UserHandle.class)));
|
||||
|
||||
doReturn(PermissionChecker.PERMISSION_HARD_DENIED).when(
|
||||
() -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext,
|
||||
Manifest.permission.SCHEDULE_EXACT_ALARM));
|
||||
when(mDeviceIdleInternal.isAppOnWhitelist(anyInt())).thenReturn(true);
|
||||
|
||||
final PendingIntent alarmPi = getNewMockPendingIntent();
|
||||
final AlarmManager.AlarmClockInfo alarmClock = mock(AlarmManager.AlarmClockInfo.class);
|
||||
try {
|
||||
mBinder.set(TEST_CALLING_PACKAGE, RTC_WAKEUP, 1234, WINDOW_EXACT, 0, 0,
|
||||
alarmPi, null, null, null, alarmClock);
|
||||
fail("alarm clock binder call succeeded without permission");
|
||||
} catch (SecurityException se) {
|
||||
// Expected.
|
||||
}
|
||||
|
||||
verify(() -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext,
|
||||
Manifest.permission.SCHEDULE_EXACT_ALARM));
|
||||
verify(mDeviceIdleInternal, never()).isAppOnWhitelist(anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exactBinderCallWithPermission() throws RemoteException {
|
||||
doReturn(true).when(
|
||||
() -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),
|
||||
anyString(), any(UserHandle.class)));
|
||||
|
||||
// Permission check is granted by default by the mock.
|
||||
final PendingIntent alarmPi = getNewMockPendingIntent();
|
||||
mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_EXACT, 0,
|
||||
0, alarmPi, null, null, null, null);
|
||||
|
||||
verify(() -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext,
|
||||
Manifest.permission.SCHEDULE_EXACT_ALARM));
|
||||
verify(mDeviceIdleInternal, never()).isAppOnWhitelist(anyInt());
|
||||
|
||||
final ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
|
||||
verify(mService).setImpl(eq(ELAPSED_REALTIME_WAKEUP), eq(1234L), eq(WINDOW_EXACT), eq(0L),
|
||||
eq(alarmPi), isNull(), isNull(),
|
||||
eq(FLAG_STANDALONE), isNull(), isNull(),
|
||||
eq(Process.myUid()), eq(TEST_CALLING_PACKAGE), bundleCaptor.capture());
|
||||
|
||||
final BroadcastOptions idleOptions = new BroadcastOptions(bundleCaptor.getValue());
|
||||
final int type = idleOptions.getTemporaryAppAllowlistType();
|
||||
assertEquals(TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED, type);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exactBinderCallWithAllowlist() throws RemoteException {
|
||||
doReturn(true).when(
|
||||
() -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),
|
||||
anyString(), any(UserHandle.class)));
|
||||
// If permission is denied, only then allowlist will be checked.
|
||||
doReturn(PermissionChecker.PERMISSION_HARD_DENIED).when(
|
||||
() -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext,
|
||||
Manifest.permission.SCHEDULE_EXACT_ALARM));
|
||||
when(mDeviceIdleInternal.isAppOnWhitelist(anyInt())).thenReturn(true);
|
||||
|
||||
final PendingIntent alarmPi = getNewMockPendingIntent();
|
||||
mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_EXACT, 0,
|
||||
0, alarmPi, null, null, null, null);
|
||||
|
||||
verify(() -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext,
|
||||
Manifest.permission.SCHEDULE_EXACT_ALARM));
|
||||
verify(mDeviceIdleInternal).isAppOnWhitelist(UserHandle.getAppId(Process.myUid()));
|
||||
|
||||
final ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
|
||||
verify(mService).setImpl(eq(ELAPSED_REALTIME_WAKEUP), eq(1234L), eq(WINDOW_EXACT), eq(0L),
|
||||
eq(alarmPi), isNull(), isNull(),
|
||||
eq(FLAG_STANDALONE), isNull(), isNull(),
|
||||
eq(Process.myUid()), eq(TEST_CALLING_PACKAGE), bundleCaptor.capture());
|
||||
System.out.println("what got captured: " + bundleCaptor.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exactAllowWhileIdleBinderCallWithPermission() throws RemoteException {
|
||||
doReturn(true).when(
|
||||
@@ -1798,48 +1906,57 @@ public class AlarmManagerServiceTest {
|
||||
|
||||
final BroadcastOptions idleOptions = new BroadcastOptions(bundleCaptor.getValue());
|
||||
final int type = idleOptions.getTemporaryAppAllowlistType();
|
||||
assertEquals(TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED, type);
|
||||
// App is on power allowlist, doesn't need explicit FGS grant in broadcast options.
|
||||
assertEquals(TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED, type);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inexactAllowWhileIdleBinderCallWithAllowlist() throws RemoteException {
|
||||
doReturn(true).when(
|
||||
() -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),
|
||||
anyString(), any(UserHandle.class)));
|
||||
|
||||
when(mDeviceIdleInternal.isAppOnWhitelist(anyInt())).thenReturn(true);
|
||||
final PendingIntent alarmPi = getNewMockPendingIntent();
|
||||
mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 4321, WINDOW_HEURISTIC, 0,
|
||||
FLAG_ALLOW_WHILE_IDLE, alarmPi, null, null, null, null);
|
||||
|
||||
verify(() -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext,
|
||||
Manifest.permission.SCHEDULE_EXACT_ALARM), never());
|
||||
verify(mDeviceIdleInternal).isAppOnWhitelist(UserHandle.getAppId(Process.myUid()));
|
||||
|
||||
final ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
|
||||
verify(mService).setImpl(eq(ELAPSED_REALTIME_WAKEUP), eq(4321L), anyLong(), eq(0L),
|
||||
eq(alarmPi), isNull(), isNull(), eq(FLAG_ALLOW_WHILE_IDLE_COMPAT), isNull(),
|
||||
isNull(), eq(Process.myUid()), eq(TEST_CALLING_PACKAGE), bundleCaptor.capture());
|
||||
|
||||
final BroadcastOptions idleOptions = new BroadcastOptions(bundleCaptor.getValue());
|
||||
final int type = idleOptions.getTemporaryAppAllowlistType();
|
||||
assertEquals(TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED, type);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inexactAllowWhileIdleBinderCallWithoutAllowlist() throws RemoteException {
|
||||
public void exactBinderCallsWithoutPermissionWithoutAllowlist() throws RemoteException {
|
||||
setDeviceConfigBoolean(KEY_CRASH_NON_CLOCK_APPS, true);
|
||||
doReturn(true).when(
|
||||
() -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),
|
||||
anyString(), any(UserHandle.class)));
|
||||
|
||||
doReturn(PermissionChecker.PERMISSION_HARD_DENIED).when(
|
||||
() -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext,
|
||||
Manifest.permission.SCHEDULE_EXACT_ALARM));
|
||||
when(mDeviceIdleInternal.isAppOnWhitelist(anyInt())).thenReturn(false);
|
||||
|
||||
final PendingIntent alarmPi = getNewMockPendingIntent();
|
||||
try {
|
||||
mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_EXACT, 0,
|
||||
0, alarmPi, null, null, null, null);
|
||||
fail("exact binder call succeeded without permission");
|
||||
} catch (SecurityException se) {
|
||||
// Expected.
|
||||
}
|
||||
try {
|
||||
mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_EXACT, 0,
|
||||
FLAG_ALLOW_WHILE_IDLE, alarmPi, null, null, null, null);
|
||||
fail("exact, allow-while-idle binder call succeeded without permission");
|
||||
} catch (SecurityException se) {
|
||||
// Expected.
|
||||
}
|
||||
verify(() -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext,
|
||||
Manifest.permission.SCHEDULE_EXACT_ALARM), times(2));
|
||||
verify(mDeviceIdleInternal, times(2)).isAppOnWhitelist(anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inexactAllowWhileIdleBinderCall() throws RemoteException {
|
||||
// Both permission and power exemption status don't matter for these alarms.
|
||||
// We only want to test that the flags and idleOptions are correct.
|
||||
doReturn(true).when(
|
||||
() -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),
|
||||
anyString(), any(UserHandle.class)));
|
||||
|
||||
final PendingIntent alarmPi = getNewMockPendingIntent();
|
||||
mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 4321, WINDOW_HEURISTIC, 0,
|
||||
FLAG_ALLOW_WHILE_IDLE, alarmPi, null, null, null, null);
|
||||
|
||||
verify(() -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext,
|
||||
Manifest.permission.SCHEDULE_EXACT_ALARM), never());
|
||||
verify(mDeviceIdleInternal).isAppOnWhitelist(UserHandle.getAppId(Process.myUid()));
|
||||
verify(mDeviceIdleInternal, never()).isAppOnWhitelist(anyInt());
|
||||
|
||||
final ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
|
||||
verify(mService).setImpl(eq(ELAPSED_REALTIME_WAKEUP), eq(4321L), anyLong(), eq(0L),
|
||||
@@ -1851,6 +1968,97 @@ public class AlarmManagerServiceTest {
|
||||
assertEquals(TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED, type);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void binderCallWithUserAllowlist() throws RemoteException {
|
||||
doReturn(true).when(
|
||||
() -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),
|
||||
anyString(), any(UserHandle.class)));
|
||||
|
||||
doReturn(PermissionChecker.PERMISSION_HARD_DENIED).when(
|
||||
() -> PermissionChecker.checkCallingOrSelfPermissionForPreflight(mMockContext,
|
||||
Manifest.permission.SCHEDULE_EXACT_ALARM));
|
||||
when(mDeviceIdleInternal.isAppOnWhitelist(anyInt())).thenReturn(true);
|
||||
when(mAppStateTracker.isUidPowerSaveUserExempt(Process.myUid())).thenReturn(true);
|
||||
|
||||
final PendingIntent alarmPi = getNewMockPendingIntent();
|
||||
mBinder.set(TEST_CALLING_PACKAGE, ELAPSED_REALTIME_WAKEUP, 1234, WINDOW_EXACT, 0,
|
||||
FLAG_ALLOW_WHILE_IDLE, alarmPi, null, null, null, null);
|
||||
|
||||
final ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
|
||||
verify(mService).setImpl(eq(ELAPSED_REALTIME_WAKEUP), eq(1234L), eq(WINDOW_EXACT), eq(0L),
|
||||
eq(alarmPi), isNull(), isNull(),
|
||||
eq(FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED | FLAG_STANDALONE), isNull(), isNull(),
|
||||
eq(Process.myUid()), eq(TEST_CALLING_PACKAGE), isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void minWindow() {
|
||||
final long minWindow = 73;
|
||||
setDeviceConfigLong(KEY_MIN_WINDOW, minWindow);
|
||||
|
||||
// 0 is WINDOW_EXACT and < 0 is WINDOW_HEURISTIC.
|
||||
for (int window = 1; window <= minWindow; window++) {
|
||||
final PendingIntent pi = getNewMockPendingIntent();
|
||||
setTestAlarm(ELAPSED_REALTIME, 0, window, pi, 0, 0, TEST_CALLING_UID, null);
|
||||
|
||||
assertEquals(1, mService.mAlarmStore.size());
|
||||
final Alarm a = mService.mAlarmStore.remove(unused -> true).get(0);
|
||||
assertEquals(minWindow, a.windowLength);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void opScheduleExactAlarmRevoked() throws Exception {
|
||||
when(mIAppOpsService.checkOperation(OP_SCHEDULE_EXACT_ALARM, TEST_CALLING_UID,
|
||||
TEST_CALLING_PACKAGE)).thenReturn(AppOpsManager.MODE_ERRORED);
|
||||
mIAppOpsCallback.opChanged(OP_SCHEDULE_EXACT_ALARM, TEST_CALLING_UID, TEST_CALLING_PACKAGE);
|
||||
assertAndHandleMessageSync(REMOVE_EXACT_ALARMS);
|
||||
verify(mService).removeExactAlarmsOnPermissionRevokedLocked(TEST_CALLING_UID,
|
||||
TEST_CALLING_PACKAGE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeExactAlarmsOnPermissionRevoked() {
|
||||
doReturn(true).when(
|
||||
() -> CompatChanges.isChangeEnabled(eq(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION),
|
||||
anyString(), any(UserHandle.class)));
|
||||
|
||||
// basic exact alarm
|
||||
setTestAlarm(ELAPSED_REALTIME, 0, 0, getNewMockPendingIntent(), 0, 0, TEST_CALLING_UID,
|
||||
null);
|
||||
// exact and allow-while-idle alarm
|
||||
setTestAlarm(ELAPSED_REALTIME, 0, 0, getNewMockPendingIntent(), 0, FLAG_ALLOW_WHILE_IDLE,
|
||||
TEST_CALLING_UID, null);
|
||||
// alarm clock
|
||||
setWakeFromIdle(RTC_WAKEUP, 0, getNewMockPendingIntent());
|
||||
|
||||
final PendingIntent inexact = getNewMockPendingIntent();
|
||||
setTestAlarm(ELAPSED_REALTIME, 0, 10, inexact, 0, 0, TEST_CALLING_UID, null);
|
||||
|
||||
final PendingIntent inexactAwi = getNewMockPendingIntent();
|
||||
setTestAlarm(ELAPSED_REALTIME, 0, 10, inexactAwi, 0, FLAG_ALLOW_WHILE_IDLE,
|
||||
TEST_CALLING_UID, null);
|
||||
|
||||
final PendingIntent exactButDifferentUid = getNewMockPendingIntent();
|
||||
setTestAlarm(ELAPSED_REALTIME, 0, 0, exactButDifferentUid, 0, 0, TEST_CALLING_UID + 5,
|
||||
null);
|
||||
assertEquals(6, mService.mAlarmStore.size());
|
||||
|
||||
mService.removeExactAlarmsOnPermissionRevokedLocked(TEST_CALLING_UID, TEST_CALLING_PACKAGE);
|
||||
|
||||
final ArrayList<Alarm> remaining = mService.mAlarmStore.asList();
|
||||
assertEquals(3, remaining.size());
|
||||
assertTrue("Basic inexact alarm removed",
|
||||
remaining.removeIf(a -> a.matches(inexact, null)));
|
||||
assertTrue("Inexact allow-while-idle alarm removed",
|
||||
remaining.removeIf(a -> a.matches(inexactAwi, null)));
|
||||
assertTrue("Alarm from different uid removed",
|
||||
remaining.removeIf(a -> a.matches(exactButDifferentUid, null)));
|
||||
|
||||
// Mock should return false by default.
|
||||
verify(mDeviceIdleInternal).isAppOnWhitelist(UserHandle.getAppId(TEST_CALLING_UID));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void idleOptionsSentOnExpiration() throws Exception {
|
||||
final long triggerTime = mNowElapsedTest + 5000;
|
||||
@@ -1858,7 +2066,7 @@ public class AlarmManagerServiceTest {
|
||||
final Bundle idleOptions = new Bundle();
|
||||
idleOptions.putChar("TEST_CHAR_KEY", 'x');
|
||||
idleOptions.putInt("TEST_INT_KEY", 53);
|
||||
setTestAlarm(ELAPSED_REALTIME_WAKEUP, triggerTime, alarmPi, 0, 0, TEST_CALLING_UID,
|
||||
setTestAlarm(ELAPSED_REALTIME_WAKEUP, triggerTime, 0, alarmPi, 0, 0, TEST_CALLING_UID,
|
||||
idleOptions);
|
||||
|
||||
mNowElapsedTest = mTestTimer.getElapsed();
|
||||
@@ -1885,6 +2093,7 @@ public class AlarmManagerServiceTest {
|
||||
assertTrue(i + "th PendingIntent missing: ",
|
||||
alarmsBefore.removeIf(a -> a.matches(pi, null)));
|
||||
}
|
||||
assertEquals(BatchingAlarmStore.TAG, mService.mAlarmStore.getName());
|
||||
|
||||
setDeviceConfigBoolean(KEY_LAZY_BATCHING, true);
|
||||
|
||||
@@ -1895,6 +2104,7 @@ public class AlarmManagerServiceTest {
|
||||
assertTrue(i + "th PendingIntent missing: ",
|
||||
alarmsAfter.removeIf(a -> a.matches(pi, null)));
|
||||
}
|
||||
assertEquals(LazyAlarmStore.TAG, mService.mAlarmStore.getName());
|
||||
}
|
||||
|
||||
@After
|
||||
|
||||
Reference in New Issue
Block a user