blob: 633b4fb2586025e452e8821279884697b7455784 [file] [log] [blame]
/*
* Copyright (C) 2018 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.app.notification.legacy.cts;
import static android.app.NotificationManager.INTERRUPTION_FILTER_ALARMS;
import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE;
import static android.service.notification.NotificationListenerService.INTERRUPTION_FILTER_PRIORITY;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNull;
import static junit.framework.TestCase.fail;
import static java.lang.Thread.sleep;
import android.app.AutomaticZenRule;
import android.app.Instrumentation;
import android.app.NotificationManager;
import android.app.UiAutomation;
import android.content.ComponentName;
import android.content.Context;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.service.notification.Condition;
import android.service.notification.ZenModeConfig;
import android.util.ArraySet;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.AmUtils;
import junit.framework.Assert;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
@RunWith(AndroidJUnit4.class)
public class ConditionProviderServiceTest {
private static String TAG = "CpsTest";
private NotificationManager mNm;
private Context mContext;
private ZenModeBroadcastReceiver mModeReceiver;
private IntentFilter mModeFilter;
private ArraySet<String> ids = new ArraySet<>();
@Before
public void setUp() throws Exception {
mContext = InstrumentationRegistry.getContext();
mModeReceiver = new ZenModeBroadcastReceiver();
mModeFilter = new IntentFilter();
mModeFilter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED);
mContext.registerReceiver(mModeReceiver, mModeFilter,
Context.RECEIVER_EXPORTED_UNAUDITED);
toggleNotificationPolicyAccess(mContext.getPackageName(),
InstrumentationRegistry.getInstrumentation(), true);
LegacyConditionProviderService.requestRebind(LegacyConditionProviderService.getId());
SecondaryConditionProviderService.requestRebind(SecondaryConditionProviderService.getId());
mNm = (NotificationManager) mContext.getSystemService(
Context.NOTIFICATION_SERVICE);
mNm.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL);
}
@After
public void tearDown() {
try {
if (mNm != null) {
try {
for (String id : ids) {
if (id != null) {
if (!mNm.removeAutomaticZenRule(id)) {
throw new Exception("Could not remove rule " + id);
}
sleep(100);
assertNull(mNm.getAutomaticZenRule(id));
}
}
} finally {
toggleNotificationPolicyAccess(mContext.getPackageName(),
InstrumentationRegistry.getInstrumentation(), false);
pollForConnection(LegacyConditionProviderService.class, false);
pollForConnection(SecondaryConditionProviderService.class, false);
}
}
if (mModeReceiver != null) {
mContext.unregisterReceiver(mModeReceiver);
}
} catch (Exception e) {
Log.e(TAG, "Error cleaning up test", e);
}
}
@Test
public void testUnboundCPSMaintainsCondition_addsNewRule() throws Exception {
// make sure service get bound
pollForConnection(SecondaryConditionProviderService.class, true);
final ComponentName cn = SecondaryConditionProviderService.getId();
// add rule
mModeReceiver.reset();
addRule(cn, INTERRUPTION_FILTER_ALARMS, true);
pollForSubscribe(SecondaryConditionProviderService.getInstance());
mModeReceiver.waitFor(1/*Secondary only*/, 1000/*Limit is 1 second*/);
assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter());
// unbind service
SecondaryConditionProviderService.getInstance().requestUnbind();
// verify that DND state doesn't change
assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter());
// add a new rule
addRule(cn, INTERRUPTION_FILTER_NONE, true);
// verify that the unbound service maintains it's DND vote
assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter());
}
@Test
public void testUnboundCPSMaintainsCondition_otherConditionChanges() throws Exception {
// make sure both services get bound
pollForConnection(LegacyConditionProviderService.class, true);
pollForConnection(SecondaryConditionProviderService.class, true);
// add rules for both
mModeReceiver.reset();
addRule(LegacyConditionProviderService.getId(), INTERRUPTION_FILTER_PRIORITY, true);
pollForSubscribe(LegacyConditionProviderService.getInstance());
addRule(SecondaryConditionProviderService.getId(), INTERRUPTION_FILTER_ALARMS, true);
pollForSubscribe(SecondaryConditionProviderService.getInstance());
mModeReceiver.waitFor(2/*Legacy and Secondary*/, 1000/*Limit is 1 second*/);
assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter());
// unbind one of the services
SecondaryConditionProviderService.getInstance().requestUnbind();
// verify that DND state doesn't change
assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter());
// trigger a change in the bound service's condition
((LegacyConditionProviderService) LegacyConditionProviderService.getInstance())
.toggleDND(false);
sleep(500);
// verify that the unbound service maintains it's DND vote
assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter());
}
@Test
public void testUnboundCPSMaintainsCondition_otherProviderRuleChanges() throws Exception {
// make sure both services get bound
pollForConnection(LegacyConditionProviderService.class, true);
pollForConnection(SecondaryConditionProviderService.class, true);
// add rules for both
mModeReceiver.reset();
addRule(LegacyConditionProviderService.getId(), INTERRUPTION_FILTER_PRIORITY, true);
pollForSubscribe(LegacyConditionProviderService.getInstance());
addRule(SecondaryConditionProviderService.getId(), INTERRUPTION_FILTER_ALARMS, true);
pollForSubscribe(SecondaryConditionProviderService.getInstance());
mModeReceiver.waitFor(2/*Legacy and Secondary*/, 1000/*Limit is 1 second*/);
assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter());
// unbind one of the services
SecondaryConditionProviderService.getInstance().requestUnbind();
// verify that DND state doesn't change
assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter());
// trigger a change in the bound service's rule
addRule(SecondaryConditionProviderService.getId(), INTERRUPTION_FILTER_PRIORITY, false);
// verify that the unbound service maintains it's DND vote
assertEquals(INTERRUPTION_FILTER_ALARMS, mNm.getCurrentInterruptionFilter());
}
@Test
public void testRequestRebindWhenLostAccess() throws Exception {
// make sure it gets bound
pollForConnection(LegacyConditionProviderService.class, true);
// request unbind
LegacyConditionProviderService.getInstance().requestUnbind();
// make sure it unbinds
pollForConnection(LegacyConditionProviderService.class, false);
// lose dnd access
toggleNotificationPolicyAccess(mContext.getPackageName(),
InstrumentationRegistry.getInstrumentation(), false);
// try to rebind
LegacyConditionProviderService.requestRebind(LegacyConditionProviderService.getId());
// make sure it isn't rebound
try {
pollForConnection(LegacyConditionProviderService.class, true);
fail("Service got rebound after permission lost");
} catch (Exception e) {
// pass
}
}
@Test
public void testRequestRebindWhenStillHasAccess() throws Exception {
// make sure it gets bound
pollForConnection(LegacyConditionProviderService.class, true);
// request unbind
LegacyConditionProviderService.getInstance().requestUnbind();
// make sure it unbinds
pollForConnection(LegacyConditionProviderService.class, false);
// try to rebind
LegacyConditionProviderService.requestRebind(LegacyConditionProviderService.getId());
// make sure it did rebind
try {
pollForConnection(LegacyConditionProviderService.class, true);
} catch (Exception e) {
fail("Service should've been able to rebind");
}
}
@Test
public void testMethodsExistAndDoNotThrow() throws Exception {
// make sure it gets bound
pollForConnection(LegacyConditionProviderService.class, true);
// request unbind
LegacyConditionProviderService.getInstance().onConnected();
LegacyConditionProviderService.getInstance().onRequestConditions(
Condition.FLAG_RELEVANT_NOW);
LegacyConditionProviderService.getInstance().onSubscribe(Uri.EMPTY);
LegacyConditionProviderService.getInstance().onUnsubscribe(Uri.EMPTY);
}
private void addRule(ComponentName cn, int filter, boolean enabled) {
final Uri conditionId = new Uri.Builder()
.scheme(Condition.SCHEME)
.authority(ZenModeConfig.SYSTEM_AUTHORITY)
.appendPath(cn.toString())
.build();
String id = mNm.addAutomaticZenRule(new AutomaticZenRule("name",
cn, conditionId, filter, enabled));
Log.d(TAG, "Created rule with id " + id);
ids.add(id);
}
private void toggleNotificationPolicyAccess(String packageName,
Instrumentation instrumentation, boolean on) throws IOException {
String command = " cmd notification " + (on ? "allow_dnd " : "disallow_dnd ") + packageName;
runCommand(command, instrumentation);
AmUtils.waitForBroadcastBarrier();
NotificationManager nm = mContext.getSystemService(NotificationManager.class);
Assert.assertEquals("Notification Policy Access Grant is " +
nm.isNotificationPolicyAccessGranted() + " not " + on, on,
nm.isNotificationPolicyAccessGranted());
}
private void runCommand(String command, Instrumentation instrumentation) throws IOException {
UiAutomation uiAutomation = instrumentation.getUiAutomation();
// Execute command
try (ParcelFileDescriptor fd = uiAutomation.executeShellCommand(command)) {
Assert.assertNotNull("Failed to execute shell command: " + command, fd);
// Wait for the command to finish by reading until EOF
try (InputStream in = new FileInputStream(fd.getFileDescriptor())) {
byte[] buffer = new byte[4096];
while (in.read(buffer) > 0) {}
} catch (IOException e) {
throw new IOException("Could not read stdout of command: " + command, e);
}
} finally {
uiAutomation.destroy();
}
}
private void pollForSubscribe(PollableConditionProviderService service) throws Exception {
int tries = 30;
int delayMs = 200;
while (tries-- > 0 && !service.subscribed) {
try {
sleep(delayMs);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (!service.subscribed) {
Log.d(TAG, "not subscribed");
throw new Exception("Service never got onSubscribe()");
}
}
private void pollForConnection(Class<? extends PollableConditionProviderService> service,
boolean waitForConnection) throws Exception {
int tries = 100;
int delayMs = 200;
PollableConditionProviderService instance =
(PollableConditionProviderService) service.getMethod("getInstance").invoke(null);
while (tries-- > 0 && (waitForConnection ? instance == null : instance != null)) {
try {
sleep(delayMs);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = (PollableConditionProviderService) service.getMethod("getInstance")
.invoke(null);
}
if (waitForConnection && instance == null) {
Log.d(TAG, service.getName() + " not bound");
throw new Exception("CPS never bound");
} else if (!waitForConnection && instance != null) {
Log.d(TAG, service.getName() + " still bound");
throw new Exception("CPS still bound");
} else {
Log.d(TAG, service.getName() + " has a correct bind state");
}
}
}