1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.app.notification.legacy.cts; 18 19 import static android.app.NotificationManager.INTERRUPTION_FILTER_ALARMS; 20 import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE; 21 import static android.service.notification.NotificationListenerService.INTERRUPTION_FILTER_PRIORITY; 22 23 import static junit.framework.Assert.assertEquals; 24 import static junit.framework.Assert.assertNull; 25 import static junit.framework.TestCase.fail; 26 27 import static java.lang.Thread.sleep; 28 29 import android.Manifest; 30 import android.app.AutomaticZenRule; 31 import android.app.Instrumentation; 32 import android.app.NotificationManager; 33 import android.app.UiAutomation; 34 import android.content.ComponentName; 35 import android.content.Context; 36 import android.content.IntentFilter; 37 import android.net.Uri; 38 import android.os.ParcelFileDescriptor; 39 import android.service.notification.Condition; 40 import android.service.notification.ZenModeConfig; 41 import android.util.ArraySet; 42 import android.util.Log; 43 44 import androidx.test.InstrumentationRegistry; 45 import androidx.test.runner.AndroidJUnit4; 46 47 import com.android.bedstead.harrier.DeviceState; 48 import com.android.bedstead.multiuser.annotations.RequireRunNotOnVisibleBackgroundNonProfileUser; 49 import com.android.compatibility.common.util.AmUtils; 50 import com.android.compatibility.common.util.SystemUtil; 51 52 import junit.framework.Assert; 53 54 import org.junit.After; 55 import org.junit.Before; 56 import org.junit.ClassRule; 57 import org.junit.Rule; 58 import org.junit.Test; 59 import org.junit.runner.RunWith; 60 61 import java.io.FileInputStream; 62 import java.io.IOException; 63 import java.io.InputStream; 64 65 @RunWith(AndroidJUnit4.class) 66 // TODO(b/355106764): Remove the annotation once zen/dnd supports visible background users. 67 @RequireRunNotOnVisibleBackgroundNonProfileUser(reason = "Zen/DND does not support visible" 68 + " background users. Only CPSes for the foreground user are bound.") 69 public class ConditionProviderServiceTest { 70 private static String TAG = "CpsTest"; 71 72 @ClassRule 73 @Rule 74 public static final DeviceState sDeviceState = new DeviceState(); 75 76 private NotificationManager mNm; 77 private Context mContext; 78 private ZenModeBroadcastReceiver mModeReceiver; 79 private IntentFilter mModeFilter; 80 private ArraySet<String> ids = new ArraySet<>(); 81 private static final int BROADCAST_TIMEOUT_MS = 5000; 82 83 @Before setUp()84 public void setUp() throws Exception { 85 mContext = InstrumentationRegistry.getContext(); 86 mModeReceiver = new ZenModeBroadcastReceiver(); 87 mModeFilter = new IntentFilter(); 88 mModeFilter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED); 89 mContext.registerReceiver(mModeReceiver, mModeFilter, Context.RECEIVER_EXPORTED); 90 toggleNotificationPolicyAccess(mContext.getPackageName(), 91 InstrumentationRegistry.getInstrumentation(), true); 92 LegacyConditionProviderService.requestRebind(LegacyConditionProviderService.getId()); 93 SecondaryConditionProviderService.requestRebind(SecondaryConditionProviderService.getId()); 94 mNm = (NotificationManager) mContext.getSystemService( 95 Context.NOTIFICATION_SERVICE); 96 97 SystemUtil.runWithShellPermissionIdentity( 98 () -> mNm.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL), 99 Manifest.permission.STATUS_BAR_SERVICE); 100 } 101 102 @After tearDown()103 public void tearDown() { 104 try { 105 if (mNm != null) { 106 try { 107 for (String id : ids) { 108 if (id != null) { 109 if (!mNm.removeAutomaticZenRule(id)) { 110 throw new Exception("Could not remove rule " + id); 111 } 112 sleep(100); 113 assertNull(mNm.getAutomaticZenRule(id)); 114 } 115 } 116 } finally { 117 toggleNotificationPolicyAccess(mContext.getPackageName(), 118 InstrumentationRegistry.getInstrumentation(), false); 119 pollForConnection(LegacyConditionProviderService.class, false); 120 pollForConnection(SecondaryConditionProviderService.class, false); 121 } 122 } 123 if (mModeReceiver != null) { 124 mContext.unregisterReceiver(mModeReceiver); 125 } 126 } catch (Exception e) { 127 Log.e(TAG, "Error cleaning up test", e); 128 } 129 } 130 131 @Test testUnboundCPSMaintainsCondition_addsNewRule()132 public void testUnboundCPSMaintainsCondition_addsNewRule() throws Exception { 133 // make sure service get bound 134 pollForConnection(SecondaryConditionProviderService.class, true); 135 136 final ComponentName cn = SecondaryConditionProviderService.getId(); 137 138 // add rule 139 mModeReceiver.reset(); 140 141 addRule(cn, INTERRUPTION_FILTER_ALARMS, true); 142 pollForSubscribe(SecondaryConditionProviderService.getInstance()); 143 144 mModeReceiver.waitFor(1/*Secondary only*/, BROADCAST_TIMEOUT_MS); 145 assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter()); 146 147 // unbind service 148 SecondaryConditionProviderService.getInstance().requestUnbind(); 149 150 // verify that DND state doesn't change 151 assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter()); 152 153 // add a new rule 154 addRule(cn, INTERRUPTION_FILTER_NONE, true); 155 156 // verify that the unbound service maintains it's DND vote 157 assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter()); 158 } 159 160 @Test testUnboundCPSMaintainsCondition_otherConditionChanges()161 public void testUnboundCPSMaintainsCondition_otherConditionChanges() throws Exception { 162 // make sure both services get bound 163 pollForConnection(LegacyConditionProviderService.class, true); 164 pollForConnection(SecondaryConditionProviderService.class, true); 165 166 // add rules for both 167 mModeReceiver.reset(); 168 169 addRule(LegacyConditionProviderService.getId(), INTERRUPTION_FILTER_PRIORITY, true); 170 pollForSubscribe(LegacyConditionProviderService.getInstance()); 171 172 addRule(SecondaryConditionProviderService.getId(), INTERRUPTION_FILTER_ALARMS, true); 173 pollForSubscribe(SecondaryConditionProviderService.getInstance()); 174 175 mModeReceiver.waitFor(2/*Legacy and Secondary*/, BROADCAST_TIMEOUT_MS); 176 assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter()); 177 178 // unbind one of the services 179 SecondaryConditionProviderService.getInstance().requestUnbind(); 180 181 // verify that DND state doesn't change 182 assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter()); 183 184 // trigger a change in the bound service's condition 185 ((LegacyConditionProviderService) LegacyConditionProviderService.getInstance()) 186 .toggleDND(false); 187 sleep(500); 188 189 // verify that the unbound service maintains it's DND vote 190 assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter()); 191 } 192 193 @Test testUnboundCPSMaintainsCondition_otherProviderRuleChanges()194 public void testUnboundCPSMaintainsCondition_otherProviderRuleChanges() throws Exception { 195 // make sure both services get bound 196 pollForConnection(LegacyConditionProviderService.class, true); 197 pollForConnection(SecondaryConditionProviderService.class, true); 198 199 // add rules for both 200 mModeReceiver.reset(); 201 202 addRule(LegacyConditionProviderService.getId(), INTERRUPTION_FILTER_PRIORITY, true); 203 pollForSubscribe(LegacyConditionProviderService.getInstance()); 204 205 addRule(SecondaryConditionProviderService.getId(), INTERRUPTION_FILTER_ALARMS, true); 206 pollForSubscribe(SecondaryConditionProviderService.getInstance()); 207 208 mModeReceiver.waitFor(2/*Legacy and Secondary*/, BROADCAST_TIMEOUT_MS); 209 assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter()); 210 211 // unbind one of the services 212 SecondaryConditionProviderService.getInstance().requestUnbind(); 213 214 // verify that DND state doesn't change 215 assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter()); 216 217 // trigger a change in the bound service's rule 218 addRule(SecondaryConditionProviderService.getId(), INTERRUPTION_FILTER_PRIORITY, false); 219 220 // verify that the unbound service maintains it's DND vote 221 assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter()); 222 } 223 224 @Test testRequestRebindWhenLostAccess()225 public void testRequestRebindWhenLostAccess() throws Exception { 226 // make sure it gets bound 227 pollForConnection(LegacyConditionProviderService.class, true); 228 229 // request unbind 230 LegacyConditionProviderService.getInstance().requestUnbind(); 231 232 // make sure it unbinds 233 pollForConnection(LegacyConditionProviderService.class, false); 234 235 // lose dnd access 236 toggleNotificationPolicyAccess(mContext.getPackageName(), 237 InstrumentationRegistry.getInstrumentation(), false); 238 239 // try to rebind 240 LegacyConditionProviderService.requestRebind(LegacyConditionProviderService.getId()); 241 242 // make sure it isn't rebound 243 try { 244 pollForConnection(LegacyConditionProviderService.class, true); 245 fail("Service got rebound after permission lost"); 246 } catch (Exception e) { 247 // pass 248 } 249 } 250 251 @Test testRequestRebindWhenStillHasAccess()252 public void testRequestRebindWhenStillHasAccess() throws Exception { 253 // make sure it gets bound 254 pollForConnection(LegacyConditionProviderService.class, true); 255 256 // request unbind 257 LegacyConditionProviderService.getInstance().requestUnbind(); 258 259 // make sure it unbinds 260 pollForConnection(LegacyConditionProviderService.class, false); 261 262 // try to rebind 263 LegacyConditionProviderService.requestRebind(LegacyConditionProviderService.getId()); 264 265 // make sure it did rebind 266 try { 267 pollForConnection(LegacyConditionProviderService.class, true); 268 } catch (Exception e) { 269 fail("Service should've been able to rebind"); 270 } 271 } 272 273 @Test testMethodsExistAndDoNotThrow()274 public void testMethodsExistAndDoNotThrow() throws Exception { 275 // make sure it gets bound 276 pollForConnection(LegacyConditionProviderService.class, true); 277 278 // request unbind 279 LegacyConditionProviderService.getInstance().onConnected(); 280 LegacyConditionProviderService.getInstance().onRequestConditions( 281 Condition.FLAG_RELEVANT_NOW); 282 LegacyConditionProviderService.getInstance().onSubscribe(Uri.EMPTY); 283 LegacyConditionProviderService.getInstance().onUnsubscribe(Uri.EMPTY); 284 285 } 286 addRule(ComponentName cn, int filter, boolean enabled)287 private void addRule(ComponentName cn, int filter, boolean enabled) { 288 final Uri conditionId = new Uri.Builder() 289 .scheme(Condition.SCHEME) 290 .authority(ZenModeConfig.SYSTEM_AUTHORITY) 291 .appendPath(cn.toString()) 292 .build(); 293 String id = mNm.addAutomaticZenRule(new AutomaticZenRule("name", 294 cn, conditionId, filter, enabled)); 295 Log.d(TAG, "Created rule with id " + id); 296 ids.add(id); 297 } 298 toggleNotificationPolicyAccess(String packageName, Instrumentation instrumentation, boolean on)299 private void toggleNotificationPolicyAccess(String packageName, 300 Instrumentation instrumentation, boolean on) throws IOException { 301 302 String command = " cmd notification " + (on ? "allow_dnd " : "disallow_dnd ") + packageName 303 + " " + mContext.getUserId(); 304 305 runCommand(command, instrumentation); 306 AmUtils.waitForBroadcastBarrier(); 307 308 NotificationManager nm = mContext.getSystemService(NotificationManager.class); 309 Assert.assertEquals("Notification Policy Access Grant is " + 310 nm.isNotificationPolicyAccessGranted() + " not " + on, on, 311 nm.isNotificationPolicyAccessGranted()); 312 } 313 runCommand(String command, Instrumentation instrumentation)314 private void runCommand(String command, Instrumentation instrumentation) throws IOException { 315 UiAutomation uiAutomation = instrumentation.getUiAutomation(); 316 // Execute command 317 try (ParcelFileDescriptor fd = uiAutomation.executeShellCommand(command)) { 318 Assert.assertNotNull("Failed to execute shell command: " + command, fd); 319 // Wait for the command to finish by reading until EOF 320 try (InputStream in = new FileInputStream(fd.getFileDescriptor())) { 321 byte[] buffer = new byte[4096]; 322 while (in.read(buffer) > 0) {} 323 } catch (IOException e) { 324 throw new IOException("Could not read stdout of command: " + command, e); 325 } 326 } finally { 327 uiAutomation.destroy(); 328 } 329 } 330 pollForSubscribe(PollableConditionProviderService service)331 private void pollForSubscribe(PollableConditionProviderService service) throws Exception { 332 int tries = 30; 333 int delayMs = 200; 334 335 while (tries-- > 0 && !service.subscribed) { 336 try { 337 sleep(delayMs); 338 } catch (InterruptedException e) { 339 e.printStackTrace(); 340 } 341 } 342 343 if (!service.subscribed) { 344 Log.d(TAG, "not subscribed"); 345 throw new Exception("Service never got onSubscribe()"); 346 } 347 } 348 pollForConnection(Class<? extends PollableConditionProviderService> service, boolean waitForConnection)349 private void pollForConnection(Class<? extends PollableConditionProviderService> service, 350 boolean waitForConnection) throws Exception { 351 int tries = 100; 352 int delayMs = 200; 353 354 PollableConditionProviderService instance = 355 (PollableConditionProviderService) service.getMethod("getInstance").invoke(null); 356 357 while (tries-- > 0 && (waitForConnection ? instance == null : instance != null)) { 358 try { 359 sleep(delayMs); 360 } catch (InterruptedException e) { 361 e.printStackTrace(); 362 } 363 instance = (PollableConditionProviderService) service.getMethod("getInstance") 364 .invoke(null); 365 } 366 367 if (waitForConnection && instance == null) { 368 Log.d(TAG, service.getName() + " not bound"); 369 throw new Exception("CPS never bound"); 370 } else if (!waitForConnection && instance != null) { 371 Log.d(TAG, service.getName() + " still bound"); 372 throw new Exception("CPS still bound"); 373 } else { 374 Log.d(TAG, service.getName() + " has a correct bind state"); 375 } 376 } 377 } 378