blob: 9daeff2af4a08313814d5b753d5dc1b9cc1f49a0 [file] [log] [blame]
/*
* 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 android.permission.cts;
import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
import static android.content.Intent.FLAG_RECEIVER_FOREGROUND;
import static android.os.Process.myUserHandle;
import static android.permission.cts.PermissionUtils.clearAppState;
import static android.permission.cts.TestUtils.eventually;
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
import static com.android.server.job.nano.JobPackageHistoryProto.START_PERIODIC_JOB;
import static com.android.server.job.nano.JobPackageHistoryProto.STOP_PERIODIC_JOB;
import static org.junit.Assert.assertNotEquals;
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.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import static java.lang.Math.max;
import static java.util.concurrent.TimeUnit.SECONDS;
import android.app.NotificationManager;
import android.app.UiAutomation;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Build;
import android.platform.test.annotations.AppModeFull;
import android.provider.DeviceConfig;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SdkSuppress;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.DeviceConfigStateChangerRule;
import com.android.compatibility.common.util.ProtoUtils;
import com.android.server.job.nano.JobPackageHistoryProto;
import com.android.server.job.nano.JobSchedulerServiceDumpProto;
import com.android.server.job.nano.JobSchedulerServiceDumpProto.RegisteredJob;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.runner.RunWith;
import java.util.List;
/**
* Base test class used for {@code NotificationListenerCheckTest} and
* {@code NotificationListenerCheckWithSafetyCenterUnsupportedTest}
*/
@RunWith(AndroidJUnit4.class)
@AppModeFull(reason = "Cannot set system settings as instant app. Also we never show a notification"
+ " listener check notification for instant apps.")
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
public class BaseNotificationListenerCheckTest {
private static final String LOG_TAG = BaseNotificationListenerCheckTest.class.getSimpleName();
private static final boolean DEBUG = false;
protected static final String TEST_APP_PKG =
"android.permission.cts.appthathasnotificationlistener";
private static final String TEST_APP_NOTIFICATION_SERVICE =
TEST_APP_PKG + ".CtsNotificationListenerService";
protected static final String TEST_APP_NOTIFICATION_LISTENER_APK =
"/data/local/tmp/cts/permissions/CtsAppThatHasNotificationListener.apk";
private static final int NOTIFICATION_LISTENER_CHECK_JOB_ID = 4;
/**
* Device config property for whether notification listener check is enabled on the device
*/
private static final String PROPERTY_NOTIFICATION_LISTENER_CHECK_ENABLED =
"notification_listener_check_enabled";
/**
* Device config property for time period in milliseconds after which current enabled
* notification
* listeners are queried
*/
private static final String PROPERTY_NOTIFICATION_LISTENER_CHECK_INTERVAL_MILLIS =
"notification_listener_check_interval_millis";
private static final Long OVERRIDE_NOTIFICATION_LISTENER_CHECK_INTERVAL_MILLIS =
SECONDS.toMillis(1);
private static final String PROPERTY_JOB_SCHEDULER_MAX_JOB_PER_RATE_LIMIT_WINDOW =
"qc_max_job_count_per_rate_limiting_window";
private static final String PROPERTY_JOB_SCHEDULER_RATE_LIMIT_WINDOW_MILLIS =
"qc_rate_limiting_window_ms";
private static final String ACTION_SET_UP_NOTIFICATION_LISTENER_CHECK =
"com.android.permissioncontroller.action.SET_UP_NOTIFICATION_LISTENER_CHECK";
private static final String NotificationListenerOnBootReceiver =
"com.android.permissioncontroller.privacysources.SetupPeriodicNotificationListenerCheck";
/**
* ID for notification shown by
* {@link com.android.permissioncontroller.privacysources.NotificationListenerCheck}.
*/
public static final int NOTIFICATION_LISTENER_CHECK_NOTIFICATION_ID = 3;
protected static final long UNEXPECTED_TIMEOUT_MILLIS = 10000;
protected static final long ENSURE_NOTIFICATION_NOT_SHOWN_EXPECTED_TIMEOUT_MILLIS = 5000;
private static final Context sContext = InstrumentationRegistry.getTargetContext();
private static final PackageManager sPackageManager = sContext.getPackageManager();
private static final UiAutomation sUiAutomation = InstrumentationRegistry.getInstrumentation()
.getUiAutomation();
private static final String PERMISSION_CONTROLLER_PKG = sContext.getPackageManager()
.getPermissionControllerPackageName();
private static List<ComponentName> sPreviouslyEnabledNotificationListeners;
// Override SafetyCenter enabled flag
@Rule
public DeviceConfigStateChangerRule sPrivacyDeviceConfigSafetyCenterEnabled =
new DeviceConfigStateChangerRule(sContext,
DeviceConfig.NAMESPACE_PRIVACY,
SafetyCenterUtils.PROPERTY_SAFETY_CENTER_ENABLED,
Boolean.toString(true));
// Override NlsCheck enabled flag
@Rule
public DeviceConfigStateChangerRule sPrivacyDeviceConfigNlsCheckEnabled =
new DeviceConfigStateChangerRule(sContext,
DeviceConfig.NAMESPACE_PRIVACY,
PROPERTY_NOTIFICATION_LISTENER_CHECK_ENABLED,
Boolean.toString(true));
// Override general notification interval from once every day to once ever 1 second
@Rule
public DeviceConfigStateChangerRule sPrivacyDeviceConfigNlsCheckIntervalMillis =
new DeviceConfigStateChangerRule(sContext,
DeviceConfig.NAMESPACE_PRIVACY,
PROPERTY_NOTIFICATION_LISTENER_CHECK_INTERVAL_MILLIS,
Long.toString(OVERRIDE_NOTIFICATION_LISTENER_CHECK_INTERVAL_MILLIS));
// Disable job scheduler throttling by allowing 300000 jobs per 30 sec
@Rule
public DeviceConfigStateChangerRule sJobSchedulerDeviceConfig1 =
new DeviceConfigStateChangerRule(sContext,
DeviceConfig.NAMESPACE_JOB_SCHEDULER,
PROPERTY_JOB_SCHEDULER_MAX_JOB_PER_RATE_LIMIT_WINDOW,
Integer.toString(3000000));
// Disable job scheduler throttling by allowing 300000 jobs per 30 sec
@Rule
public DeviceConfigStateChangerRule sJobSchedulerDeviceConfig2 =
new DeviceConfigStateChangerRule(sContext,
DeviceConfig.NAMESPACE_JOB_SCHEDULER,
PROPERTY_JOB_SCHEDULER_RATE_LIMIT_WINDOW_MILLIS,
Integer.toString(30000));
@Rule
public CtsNotificationListenerHelperRule ctsNotificationListenerHelper =
new CtsNotificationListenerHelperRule(sContext);
@BeforeClass
public static void beforeClassSetup() throws Exception {
// Disallow any OEM enabled NLS
disallowPreexistingNotificationListeners();
}
@AfterClass
public static void afterClassTearDown() throws Throwable {
// Reallow any previously OEM allowed NLS
reallowPreexistingNotificationListeners();
}
private static void setDeviceConfigPrivacyProperty(String propertyName, String value) {
runWithShellPermissionIdentity(() -> {
boolean valueWasSet = DeviceConfig.setProperty(
DeviceConfig.NAMESPACE_PRIVACY,
/* name = */ propertyName,
/* value = */ value,
/* makeDefault = */ false);
if (!valueWasSet) {
throw new IllegalStateException("Could not set " + propertyName + " to " + value);
}
}, WRITE_DEVICE_CONFIG);
}
/**
* Enable or disable notification listener check
*/
protected static void setNotificationListenerCheckEnabled(boolean enabled) {
setDeviceConfigPrivacyProperty(
PROPERTY_NOTIFICATION_LISTENER_CHECK_ENABLED,
/* value = */ String.valueOf(enabled));
}
/**
* Allow or disallow a {@link NotificationListenerService} component for the current user
*
* @param listenerComponent {@link NotificationListenerService} component to allow or disallow
*/
private static void setNotificationListenerServiceAllowed(ComponentName listenerComponent,
boolean allowed) {
String command = " cmd notification " + (allowed ? "allow_listener " : "disallow_listener ")
+ listenerComponent.flattenToString();
runShellCommand(command);
}
private static void disallowPreexistingNotificationListeners() {
runWithShellPermissionIdentity(() -> {
NotificationManager notificationManager =
sContext.getSystemService(NotificationManager.class);
sPreviouslyEnabledNotificationListeners =
notificationManager.getEnabledNotificationListeners();
});
if (DEBUG) {
Log.d(LOG_TAG, "Found " + sPreviouslyEnabledNotificationListeners.size()
+ " previously allowed notification listeners. Disabling before test run.");
}
for (ComponentName listener : sPreviouslyEnabledNotificationListeners) {
setNotificationListenerServiceAllowed(listener, false);
}
}
private static void reallowPreexistingNotificationListeners() {
if (DEBUG) {
Log.d(LOG_TAG, "Re-allowing " + sPreviouslyEnabledNotificationListeners.size()
+ " previously allowed notification listeners found before test run.");
}
for (ComponentName listener : sPreviouslyEnabledNotificationListeners) {
setNotificationListenerServiceAllowed(listener, true);
}
}
protected void allowTestAppNotificationListenerService() {
setNotificationListenerServiceAllowed(
new ComponentName(TEST_APP_PKG, TEST_APP_NOTIFICATION_SERVICE), true);
}
protected void disallowTestAppNotificationListenerService() {
setNotificationListenerServiceAllowed(
new ComponentName(TEST_APP_PKG, TEST_APP_NOTIFICATION_SERVICE), false);
}
/**
* Get the state of the job scheduler
*/
private static JobSchedulerServiceDumpProto getJobSchedulerDump() throws Exception {
return ProtoUtils.getProto(sUiAutomation, JobSchedulerServiceDumpProto.class,
ProtoUtils.DUMPSYS_JOB_SCHEDULER);
}
/**
* Get the last time the NOTIFICATION_LISTENER_CHECK_JOB_ID job was started/stopped for
* permission
* controller.
*
* @param event the job event (start/stop)
* @return the last time the event happened.
*/
private static long getLastJobTime(int event) throws Exception {
int permControllerUid = sPackageManager.getPackageUid(PERMISSION_CONTROLLER_PKG, 0);
long lastTime = -1;
for (JobPackageHistoryProto.HistoryEvent historyEvent :
getJobSchedulerDump().history.historyEvent) {
if (historyEvent.uid == permControllerUid
&& historyEvent.jobId == NOTIFICATION_LISTENER_CHECK_JOB_ID
&& historyEvent.event == event) {
lastTime = max(lastTime,
System.currentTimeMillis() - historyEvent.timeSinceEventMs);
}
}
return lastTime;
}
/**
* Force a run of the notification listener check.
*/
protected static void runNotificationListenerCheck() throws Throwable {
// Sleep a little to make sure we don't have overlap in timing
Thread.sleep(1000);
long beforeJob = System.currentTimeMillis();
// Sleep a little to avoid raciness in time keeping
Thread.sleep(1000);
runShellCommand("cmd jobscheduler run -u " + myUserHandle().getIdentifier() + " -f "
+ PERMISSION_CONTROLLER_PKG + " " + NOTIFICATION_LISTENER_CHECK_JOB_ID);
eventually(() -> {
long startTime = getLastJobTime(START_PERIODIC_JOB);
assertTrue(startTime + " !> " + beforeJob, startTime > beforeJob);
}, UNEXPECTED_TIMEOUT_MILLIS);
// We can't simply require startTime <= endTime because the time being reported isn't
// accurate, and sometimes the end time may come before the start time by around 100 ms.
eventually(() -> {
long stopTime = getLastJobTime(STOP_PERIODIC_JOB);
assertTrue(stopTime + " !> " + beforeJob, stopTime > beforeJob);
}, UNEXPECTED_TIMEOUT_MILLIS);
}
/**
* Skip tests for if Safety Center not supported
*/
protected void assumeDeviceSupportsSafetyCenter() {
assumeTrue(SafetyCenterUtils.deviceSupportsSafetyCenter(sContext));
}
/**
* Skip tests for if Safety Center IS supported
*/
protected void assumeDeviceDoesNotSupportSafetyCenter() {
assumeFalse(SafetyCenterUtils.deviceSupportsSafetyCenter(sContext));
}
protected void wakeUpAndDismissKeyguard() {
runShellCommand("input keyevent KEYCODE_WAKEUP");
runShellCommand("wm dismiss-keyguard");
}
/**
* Reset the permission controllers state before each test
*/
protected void resetPermissionControllerBeforeEachTest() throws Throwable {
resetPermissionController();
// ensure no posted notification listener notifications exits
eventually(() -> assertNull(getNotification(false)), UNEXPECTED_TIMEOUT_MILLIS);
// Reset job scheduler stats (to allow more jobs to be run)
runShellCommand(
"cmd jobscheduler reset-execution-quota -u " + myUserHandle().getIdentifier() + " "
+ PERMISSION_CONTROLLER_PKG);
}
/**
* Reset the permission controllers state.
*/
private static void resetPermissionController() throws Throwable {
clearAppState(PERMISSION_CONTROLLER_PKG);
int currentUserId = myUserHandle().getIdentifier();
// Wait until jobs are cleared
eventually(() -> {
JobSchedulerServiceDumpProto dump = getJobSchedulerDump();
for (RegisteredJob job : dump.registeredJobs) {
if (job.dump.sourceUserId == currentUserId) {
assertNotEquals(job.dump.sourcePackageName, PERMISSION_CONTROLLER_PKG);
}
}
}, UNEXPECTED_TIMEOUT_MILLIS);
// Setup up permission controller again (simulate a reboot)
Intent permissionControllerSetupIntent = new Intent(
ACTION_SET_UP_NOTIFICATION_LISTENER_CHECK).setPackage(
PERMISSION_CONTROLLER_PKG).setFlags(FLAG_RECEIVER_FOREGROUND);
// Query for the setup broadcast receiver
List<ResolveInfo> resolveInfos = sContext.getPackageManager().queryBroadcastReceivers(
permissionControllerSetupIntent, 0);
if (resolveInfos.size() > 0) {
sContext.sendBroadcast(permissionControllerSetupIntent);
} else {
sContext.sendBroadcast(new Intent()
.setClassName(PERMISSION_CONTROLLER_PKG, NotificationListenerOnBootReceiver)
.setFlags(FLAG_RECEIVER_FOREGROUND)
.setPackage(PERMISSION_CONTROLLER_PKG));
}
// Wait until jobs are set up
eventually(() -> {
JobSchedulerServiceDumpProto dump = getJobSchedulerDump();
for (RegisteredJob job : dump.registeredJobs) {
if (job.dump.sourceUserId == currentUserId
&& job.dump.sourcePackageName.equals(PERMISSION_CONTROLLER_PKG)
&& job.dump.jobInfo.service.className.contains(
"NotificationListenerCheck")) {
return;
}
}
fail("Permission controller jobs not found");
}, UNEXPECTED_TIMEOUT_MILLIS);
}
/**
* Preshow/dismiss cts NotificationListener notification as it negatively affects test results
* (can result in unexpected test pass/failures)
*/
protected void triggerAndDismissCtsNotificationListenerNotification() throws Throwable {
// CtsNotificationListenerService isn't enabled at this point, but NotificationListener
// should be. Mark as notified by showing and dismissing
runNotificationListenerCheck();
// Ensure notification shows and dismiss
eventually(() -> assertNotNull(getNotification(true)),
UNEXPECTED_TIMEOUT_MILLIS);
}
/**
* Get a notification listener notification that is currently visible.
*
* @param cancelNotification if `true` the notification is canceled inside this method
* @return The notification or `null` if there is none
*/
protected StatusBarNotification getNotification(boolean cancelNotification) throws Throwable {
return NotificationUtils.getNotificationForPackageAndId(
PERMISSION_CONTROLLER_PKG,
NOTIFICATION_LISTENER_CHECK_NOTIFICATION_ID,
cancelNotification);
}
/**
* Clear any notifications related to NotificationListenerCheck to ensure clean test setup
*/
protected void clearNotifications() throws Throwable {
// Clear notification if present
NotificationUtils.clearNotificationsForPackage(PERMISSION_CONTROLLER_PKG);
}
}