| /* |
| * 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"; |
| |
| protected 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 NotificationListenerUtils.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 |
| NotificationListenerUtils.cancelNotifications(PERMISSION_CONTROLLER_PKG); |
| } |
| } |