/* * Copyright (C) 2023 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 com.android.server.notification; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; import android.content.ComponentName; import android.net.Uri; import android.provider.Settings; import android.service.notification.Condition; import android.service.notification.ZenModeConfig; import android.service.notification.ZenModeDiff; import android.service.notification.ZenPolicy; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.util.ArrayMap; import androidx.test.filters.SmallTest; import com.android.server.UiServiceTestCase; import org.junit.Test; import org.junit.runner.RunWith; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.List; import java.util.Set; @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper public class ZenModeDiffTest extends UiServiceTestCase { // version is not included in the diff; manual & automatic rules have special handling public static final Set ZEN_MODE_CONFIG_EXEMPT_FIELDS = Set.of("version", "manualRule", "automaticRules"); @Test public void testRuleDiff_addRemoveSame() { // Test add, remove, and both sides same ZenModeConfig.ZenRule r = makeRule(); // Both sides same rule ZenModeDiff.RuleDiff dSame = new ZenModeDiff.RuleDiff(r, r); assertFalse(dSame.hasDiff()); // from existent rule to null: expect deleted ZenModeDiff.RuleDiff deleted = new ZenModeDiff.RuleDiff(r, null); assertTrue(deleted.hasDiff()); assertTrue(deleted.wasRemoved()); // from null to new rule: expect added ZenModeDiff.RuleDiff added = new ZenModeDiff.RuleDiff(null, r); assertTrue(added.hasDiff()); assertTrue(added.wasAdded()); } @Test public void testRuleDiff_fieldDiffs() throws Exception { // Start these the same ZenModeConfig.ZenRule r1 = makeRule(); ZenModeConfig.ZenRule r2 = makeRule(); // maps mapping field name -> expected output value as we set diffs ArrayMap expectedFrom = new ArrayMap<>(); ArrayMap expectedTo = new ArrayMap<>(); List fieldsForDiff = getFieldsForDiffCheck( ZenModeConfig.ZenRule.class, Set.of()); // actually no exempt fields for ZenRule generateFieldDiffs(r1, r2, fieldsForDiff, expectedFrom, expectedTo); ZenModeDiff.RuleDiff d = new ZenModeDiff.RuleDiff(r1, r2); assertTrue(d.hasDiff()); // Now diff them and check that each of the fields has a diff for (Field f : fieldsForDiff) { String name = f.getName(); assertNotNull("diff not found for field: " + name, d.getDiffForField(name)); assertTrue(d.getDiffForField(name).hasDiff()); assertTrue("unexpected field: " + name, expectedFrom.containsKey(name)); assertTrue("unexpected field: " + name, expectedTo.containsKey(name)); assertEquals(expectedFrom.get(name), d.getDiffForField(name).from()); assertEquals(expectedTo.get(name), d.getDiffForField(name).to()); } } @Test public void testConfigDiff_addRemoveSame() { // Default config, will test add, remove, and no change ZenModeConfig c = new ZenModeConfig(); ZenModeDiff.ConfigDiff dSame = new ZenModeDiff.ConfigDiff(c, c); assertFalse(dSame.hasDiff()); ZenModeDiff.ConfigDiff added = new ZenModeDiff.ConfigDiff(null, c); assertTrue(added.hasDiff()); assertTrue(added.wasAdded()); ZenModeDiff.ConfigDiff removed = new ZenModeDiff.ConfigDiff(c, null); assertTrue(removed.hasDiff()); assertTrue(removed.wasRemoved()); } @Test public void testConfigDiff_fieldDiffs() throws Exception { // these two start the same ZenModeConfig c1 = new ZenModeConfig(); ZenModeConfig c2 = new ZenModeConfig(); // maps mapping field name -> expected output value as we set diffs ArrayMap expectedFrom = new ArrayMap<>(); ArrayMap expectedTo = new ArrayMap<>(); List fieldsForDiff = getFieldsForDiffCheck( ZenModeConfig.class, ZEN_MODE_CONFIG_EXEMPT_FIELDS); generateFieldDiffs(c1, c2, fieldsForDiff, expectedFrom, expectedTo); ZenModeDiff.ConfigDiff d = new ZenModeDiff.ConfigDiff(c1, c2); assertTrue(d.hasDiff()); // Now diff them and check that each of the fields has a diff for (Field f : fieldsForDiff) { String name = f.getName(); assertNotNull("diff not found for field: " + name, d.getDiffForField(name)); assertTrue(d.getDiffForField(name).hasDiff()); assertTrue("unexpected field: " + name, expectedFrom.containsKey(name)); assertTrue("unexpected field: " + name, expectedTo.containsKey(name)); assertEquals(expectedFrom.get(name), d.getDiffForField(name).from()); assertEquals(expectedTo.get(name), d.getDiffForField(name).to()); } } @Test public void testConfigDiff_specialSenders() { // these two start the same ZenModeConfig c1 = new ZenModeConfig(); ZenModeConfig c2 = new ZenModeConfig(); // set c1 and c2 to have some different senders c1.allowMessagesFrom = ZenModeConfig.SOURCE_STAR; c2.allowMessagesFrom = ZenModeConfig.SOURCE_CONTACT; c1.allowConversationsFrom = ZenPolicy.CONVERSATION_SENDERS_IMPORTANT; c2.allowConversationsFrom = ZenPolicy.CONVERSATION_SENDERS_NONE; ZenModeDiff.ConfigDiff d = new ZenModeDiff.ConfigDiff(c1, c2); assertTrue(d.hasDiff()); // Diff in top-level fields assertTrue(d.getDiffForField("allowMessagesFrom").hasDiff()); assertTrue(d.getDiffForField("allowConversationsFrom").hasDiff()); // Bonus testing of stringification of people senders and conversation senders assertTrue(d.toString().contains("allowMessagesFrom:stars->contacts")); assertTrue(d.toString().contains("allowConversationsFrom:important->none")); } @Test public void testConfigDiff_hasRuleDiffs() { // two default configs ZenModeConfig c1 = new ZenModeConfig(); ZenModeConfig c2 = new ZenModeConfig(); // two initially-identical rules ZenModeConfig.ZenRule r1 = makeRule(); ZenModeConfig.ZenRule r2 = makeRule(); // one that will become a manual rule ZenModeConfig.ZenRule m = makeRule(); // Add r1 to c1, but not r2 to c2 yet -- expect a rule to be deleted c1.automaticRules.put(r1.id, r1); ZenModeDiff.ConfigDiff deleteRule = new ZenModeDiff.ConfigDiff(c1, c2); assertTrue(deleteRule.hasDiff()); assertNotNull(deleteRule.getAllAutomaticRuleDiffs()); assertTrue(deleteRule.getAllAutomaticRuleDiffs().containsKey("ruleId")); assertTrue(deleteRule.getAllAutomaticRuleDiffs().get("ruleId").wasRemoved()); // Change r2 a little, add r2 to c2 as an automatic rule and m as a manual rule r2.component = null; r2.pkg = "different"; c2.manualRule = m; c2.automaticRules.put(r2.id, r2); // Expect diffs in both manual rule (added) and automatic rule (changed) ZenModeDiff.ConfigDiff changed = new ZenModeDiff.ConfigDiff(c1, c2); assertTrue(changed.hasDiff()); assertTrue(changed.getManualRuleDiff().hasDiff()); ArrayMap automaticDiffs = changed.getAllAutomaticRuleDiffs(); assertNotNull(automaticDiffs); assertTrue(automaticDiffs.containsKey("ruleId")); assertNotNull(automaticDiffs.get("ruleId").getDiffForField("component")); assertNull(automaticDiffs.get("ruleId").getDiffForField("component").to()); assertNotNull(automaticDiffs.get("ruleId").getDiffForField("pkg")); assertEquals("different", automaticDiffs.get("ruleId").getDiffForField("pkg").to()); } // Helper methods for working with configs, policies, rules // Just makes a zen rule with fields filled in private ZenModeConfig.ZenRule makeRule() { ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); rule.configurationActivity = new ComponentName("a", "a"); rule.component = new ComponentName("b", "b"); rule.conditionId = new Uri.Builder().scheme("hello").build(); rule.condition = new Condition(rule.conditionId, "", Condition.STATE_TRUE); rule.enabled = true; rule.creationTime = 123; rule.id = "ruleId"; rule.zenMode = Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; rule.modified = false; rule.name = "name"; rule.snoozing = true; rule.pkg = "a"; return rule; } // Get the fields on which we would want to check a diff. The requirements are: not final or/ // static (as these should/can never change), and not in a specific list that's exempted. private List getFieldsForDiffCheck(Class c, Set exemptNames) throws SecurityException { Field[] fields = c.getDeclaredFields(); ArrayList out = new ArrayList<>(); for (Field field : fields) { // Check for exempt reasons int m = field.getModifiers(); if (Modifier.isFinal(m) || Modifier.isStatic(m) || exemptNames.contains(field.getName())) { continue; } out.add(field); } return out; } // Generate a set of generic diffs for the specified two objects and the fields to generate // diffs for, and store the results in the provided expectation maps to be able to check the // output later. private void generateFieldDiffs(Object a, Object b, List fields, ArrayMap expectedA, ArrayMap expectedB) throws Exception { // different classes passed in means bad input assertEquals(a.getClass(), b.getClass()); // Loop through fields for which we want to check diffs, set a diff and keep track of // what we set. for (Field f : fields) { f.setAccessible(true); // Just double-check also that the fields actually are for the class declared assertEquals(f.getDeclaringClass(), a.getClass()); Class t = f.getType(); // handle the full set of primitive types first if (boolean.class.equals(t)) { f.setBoolean(a, true); expectedA.put(f.getName(), true); f.setBoolean(b, false); expectedB.put(f.getName(), false); } else if (int.class.equals(t)) { // these are not actually valid going to be valid for arbitrary int enum fields, but // we just put something in there regardless. f.setInt(a, 2); expectedA.put(f.getName(), 2); f.setInt(b, 1); expectedB.put(f.getName(), 1); } else if (long.class.equals(t)) { f.setLong(a, 200L); expectedA.put(f.getName(), 200L); f.setLong(b, 100L); expectedB.put(f.getName(), 100L); } else if (t.isPrimitive()) { // This method doesn't yet handle other primitive types. If the relevant diff // classes gain new fields of these types, please add another clause here. fail("primitive type not handled by generateFieldDiffs: " + t.getName()); } else if (String.class.equals(t)) { f.set(a, "string1"); expectedA.put(f.getName(), "string1"); f.set(b, "string2"); expectedB.put(f.getName(), "string2"); } else { // catch-all for other types: have the field be "added" f.set(a, null); expectedA.put(f.getName(), null); try { f.set(b, t.getDeclaredConstructor().newInstance()); expectedB.put(f.getName(), t.getDeclaredConstructor().newInstance()); } catch (Exception e) { // No default constructor, or blithely attempting to construct something doesn't // work for some reason. If the default value isn't null, then keep it. if (f.get(b) != null) { expectedB.put(f.getName(), f.get(b)); } else { // If we can't even rely on that, fail. Have the test-writer special case // something, as this is not able to be genericized. fail("could not generically construct value for field: " + f.getName()); } } } } } }