1 /* 2 * Copyright (C) 2023 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 com.android.server.notification; 18 19 import static junit.framework.Assert.assertEquals; 20 import static junit.framework.Assert.assertFalse; 21 import static junit.framework.Assert.assertNotNull; 22 import static junit.framework.Assert.assertNull; 23 import static junit.framework.Assert.assertTrue; 24 import static junit.framework.Assert.fail; 25 26 import android.content.ComponentName; 27 import android.net.Uri; 28 import android.provider.Settings; 29 import android.service.notification.Condition; 30 import android.service.notification.ZenModeConfig; 31 import android.service.notification.ZenModeDiff; 32 import android.service.notification.ZenPolicy; 33 import android.testing.AndroidTestingRunner; 34 import android.testing.TestableLooper; 35 import android.util.ArrayMap; 36 37 import androidx.test.filters.SmallTest; 38 39 import com.android.server.UiServiceTestCase; 40 41 import org.junit.Test; 42 import org.junit.runner.RunWith; 43 44 import java.lang.reflect.Field; 45 import java.lang.reflect.Modifier; 46 import java.util.ArrayList; 47 import java.util.List; 48 import java.util.Set; 49 50 @SmallTest 51 @RunWith(AndroidTestingRunner.class) 52 @TestableLooper.RunWithLooper 53 public class ZenModeDiffTest extends UiServiceTestCase { 54 // version is not included in the diff; manual & automatic rules have special handling 55 public static final Set<String> ZEN_MODE_CONFIG_EXEMPT_FIELDS = 56 Set.of("version", "manualRule", "automaticRules"); 57 58 @Test testRuleDiff_addRemoveSame()59 public void testRuleDiff_addRemoveSame() { 60 // Test add, remove, and both sides same 61 ZenModeConfig.ZenRule r = makeRule(); 62 63 // Both sides same rule 64 ZenModeDiff.RuleDiff dSame = new ZenModeDiff.RuleDiff(r, r); 65 assertFalse(dSame.hasDiff()); 66 67 // from existent rule to null: expect deleted 68 ZenModeDiff.RuleDiff deleted = new ZenModeDiff.RuleDiff(r, null); 69 assertTrue(deleted.hasDiff()); 70 assertTrue(deleted.wasRemoved()); 71 72 // from null to new rule: expect added 73 ZenModeDiff.RuleDiff added = new ZenModeDiff.RuleDiff(null, r); 74 assertTrue(added.hasDiff()); 75 assertTrue(added.wasAdded()); 76 } 77 78 @Test testRuleDiff_fieldDiffs()79 public void testRuleDiff_fieldDiffs() throws Exception { 80 // Start these the same 81 ZenModeConfig.ZenRule r1 = makeRule(); 82 ZenModeConfig.ZenRule r2 = makeRule(); 83 84 // maps mapping field name -> expected output value as we set diffs 85 ArrayMap<String, Object> expectedFrom = new ArrayMap<>(); 86 ArrayMap<String, Object> expectedTo = new ArrayMap<>(); 87 List<Field> fieldsForDiff = getFieldsForDiffCheck( 88 ZenModeConfig.ZenRule.class, Set.of()); // actually no exempt fields for ZenRule 89 generateFieldDiffs(r1, r2, fieldsForDiff, expectedFrom, expectedTo); 90 91 ZenModeDiff.RuleDiff d = new ZenModeDiff.RuleDiff(r1, r2); 92 assertTrue(d.hasDiff()); 93 94 // Now diff them and check that each of the fields has a diff 95 for (Field f : fieldsForDiff) { 96 String name = f.getName(); 97 assertNotNull("diff not found for field: " + name, d.getDiffForField(name)); 98 assertTrue(d.getDiffForField(name).hasDiff()); 99 assertTrue("unexpected field: " + name, expectedFrom.containsKey(name)); 100 assertTrue("unexpected field: " + name, expectedTo.containsKey(name)); 101 assertEquals(expectedFrom.get(name), d.getDiffForField(name).from()); 102 assertEquals(expectedTo.get(name), d.getDiffForField(name).to()); 103 } 104 } 105 106 @Test testConfigDiff_addRemoveSame()107 public void testConfigDiff_addRemoveSame() { 108 // Default config, will test add, remove, and no change 109 ZenModeConfig c = new ZenModeConfig(); 110 111 ZenModeDiff.ConfigDiff dSame = new ZenModeDiff.ConfigDiff(c, c); 112 assertFalse(dSame.hasDiff()); 113 114 ZenModeDiff.ConfigDiff added = new ZenModeDiff.ConfigDiff(null, c); 115 assertTrue(added.hasDiff()); 116 assertTrue(added.wasAdded()); 117 118 ZenModeDiff.ConfigDiff removed = new ZenModeDiff.ConfigDiff(c, null); 119 assertTrue(removed.hasDiff()); 120 assertTrue(removed.wasRemoved()); 121 } 122 123 @Test testConfigDiff_fieldDiffs()124 public void testConfigDiff_fieldDiffs() throws Exception { 125 // these two start the same 126 ZenModeConfig c1 = new ZenModeConfig(); 127 ZenModeConfig c2 = new ZenModeConfig(); 128 129 // maps mapping field name -> expected output value as we set diffs 130 ArrayMap<String, Object> expectedFrom = new ArrayMap<>(); 131 ArrayMap<String, Object> expectedTo = new ArrayMap<>(); 132 List<Field> fieldsForDiff = getFieldsForDiffCheck( 133 ZenModeConfig.class, ZEN_MODE_CONFIG_EXEMPT_FIELDS); 134 generateFieldDiffs(c1, c2, fieldsForDiff, expectedFrom, expectedTo); 135 136 ZenModeDiff.ConfigDiff d = new ZenModeDiff.ConfigDiff(c1, c2); 137 assertTrue(d.hasDiff()); 138 139 // Now diff them and check that each of the fields has a diff 140 for (Field f : fieldsForDiff) { 141 String name = f.getName(); 142 assertNotNull("diff not found for field: " + name, d.getDiffForField(name)); 143 assertTrue(d.getDiffForField(name).hasDiff()); 144 assertTrue("unexpected field: " + name, expectedFrom.containsKey(name)); 145 assertTrue("unexpected field: " + name, expectedTo.containsKey(name)); 146 assertEquals(expectedFrom.get(name), d.getDiffForField(name).from()); 147 assertEquals(expectedTo.get(name), d.getDiffForField(name).to()); 148 } 149 } 150 151 @Test testConfigDiff_specialSenders()152 public void testConfigDiff_specialSenders() { 153 // these two start the same 154 ZenModeConfig c1 = new ZenModeConfig(); 155 ZenModeConfig c2 = new ZenModeConfig(); 156 157 // set c1 and c2 to have some different senders 158 c1.allowMessagesFrom = ZenModeConfig.SOURCE_STAR; 159 c2.allowMessagesFrom = ZenModeConfig.SOURCE_CONTACT; 160 c1.allowConversationsFrom = ZenPolicy.CONVERSATION_SENDERS_IMPORTANT; 161 c2.allowConversationsFrom = ZenPolicy.CONVERSATION_SENDERS_NONE; 162 163 ZenModeDiff.ConfigDiff d = new ZenModeDiff.ConfigDiff(c1, c2); 164 assertTrue(d.hasDiff()); 165 166 // Diff in top-level fields 167 assertTrue(d.getDiffForField("allowMessagesFrom").hasDiff()); 168 assertTrue(d.getDiffForField("allowConversationsFrom").hasDiff()); 169 170 // Bonus testing of stringification of people senders and conversation senders 171 assertTrue(d.toString().contains("allowMessagesFrom:stars->contacts")); 172 assertTrue(d.toString().contains("allowConversationsFrom:important->none")); 173 } 174 175 @Test testConfigDiff_hasRuleDiffs()176 public void testConfigDiff_hasRuleDiffs() { 177 // two default configs 178 ZenModeConfig c1 = new ZenModeConfig(); 179 ZenModeConfig c2 = new ZenModeConfig(); 180 181 // two initially-identical rules 182 ZenModeConfig.ZenRule r1 = makeRule(); 183 ZenModeConfig.ZenRule r2 = makeRule(); 184 185 // one that will become a manual rule 186 ZenModeConfig.ZenRule m = makeRule(); 187 188 // Add r1 to c1, but not r2 to c2 yet -- expect a rule to be deleted 189 c1.automaticRules.put(r1.id, r1); 190 ZenModeDiff.ConfigDiff deleteRule = new ZenModeDiff.ConfigDiff(c1, c2); 191 assertTrue(deleteRule.hasDiff()); 192 assertNotNull(deleteRule.getAllAutomaticRuleDiffs()); 193 assertTrue(deleteRule.getAllAutomaticRuleDiffs().containsKey("ruleId")); 194 assertTrue(deleteRule.getAllAutomaticRuleDiffs().get("ruleId").wasRemoved()); 195 196 // Change r2 a little, add r2 to c2 as an automatic rule and m as a manual rule 197 r2.component = null; 198 r2.pkg = "different"; 199 c2.manualRule = m; 200 c2.automaticRules.put(r2.id, r2); 201 202 // Expect diffs in both manual rule (added) and automatic rule (changed) 203 ZenModeDiff.ConfigDiff changed = new ZenModeDiff.ConfigDiff(c1, c2); 204 assertTrue(changed.hasDiff()); 205 assertTrue(changed.getManualRuleDiff().hasDiff()); 206 207 ArrayMap<String, ZenModeDiff.RuleDiff> automaticDiffs = changed.getAllAutomaticRuleDiffs(); 208 assertNotNull(automaticDiffs); 209 assertTrue(automaticDiffs.containsKey("ruleId")); 210 assertNotNull(automaticDiffs.get("ruleId").getDiffForField("component")); 211 assertNull(automaticDiffs.get("ruleId").getDiffForField("component").to()); 212 assertNotNull(automaticDiffs.get("ruleId").getDiffForField("pkg")); 213 assertEquals("different", automaticDiffs.get("ruleId").getDiffForField("pkg").to()); 214 } 215 216 // Helper methods for working with configs, policies, rules 217 // Just makes a zen rule with fields filled in makeRule()218 private ZenModeConfig.ZenRule makeRule() { 219 ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); 220 rule.configurationActivity = new ComponentName("a", "a"); 221 rule.component = new ComponentName("b", "b"); 222 rule.conditionId = new Uri.Builder().scheme("hello").build(); 223 rule.condition = new Condition(rule.conditionId, "", Condition.STATE_TRUE); 224 rule.enabled = true; 225 rule.creationTime = 123; 226 rule.id = "ruleId"; 227 rule.zenMode = Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; 228 rule.modified = false; 229 rule.name = "name"; 230 rule.snoozing = true; 231 rule.pkg = "a"; 232 return rule; 233 } 234 235 // Get the fields on which we would want to check a diff. The requirements are: not final or/ 236 // static (as these should/can never change), and not in a specific list that's exempted. getFieldsForDiffCheck(Class c, Set<String> exemptNames)237 private List<Field> getFieldsForDiffCheck(Class c, Set<String> exemptNames) 238 throws SecurityException { 239 Field[] fields = c.getDeclaredFields(); 240 ArrayList<Field> out = new ArrayList<>(); 241 242 for (Field field : fields) { 243 // Check for exempt reasons 244 int m = field.getModifiers(); 245 if (Modifier.isFinal(m) 246 || Modifier.isStatic(m) 247 || exemptNames.contains(field.getName())) { 248 continue; 249 } 250 out.add(field); 251 } 252 return out; 253 } 254 255 // Generate a set of generic diffs for the specified two objects and the fields to generate 256 // diffs for, and store the results in the provided expectation maps to be able to check the 257 // output later. generateFieldDiffs(Object a, Object b, List<Field> fields, ArrayMap<String, Object> expectedA, ArrayMap<String, Object> expectedB)258 private void generateFieldDiffs(Object a, Object b, List<Field> fields, 259 ArrayMap<String, Object> expectedA, ArrayMap<String, Object> expectedB) 260 throws Exception { 261 // different classes passed in means bad input 262 assertEquals(a.getClass(), b.getClass()); 263 264 // Loop through fields for which we want to check diffs, set a diff and keep track of 265 // what we set. 266 for (Field f : fields) { 267 f.setAccessible(true); 268 // Just double-check also that the fields actually are for the class declared 269 assertEquals(f.getDeclaringClass(), a.getClass()); 270 Class t = f.getType(); 271 // handle the full set of primitive types first 272 if (boolean.class.equals(t)) { 273 f.setBoolean(a, true); 274 expectedA.put(f.getName(), true); 275 f.setBoolean(b, false); 276 expectedB.put(f.getName(), false); 277 } else if (int.class.equals(t)) { 278 // these are not actually valid going to be valid for arbitrary int enum fields, but 279 // we just put something in there regardless. 280 f.setInt(a, 2); 281 expectedA.put(f.getName(), 2); 282 f.setInt(b, 1); 283 expectedB.put(f.getName(), 1); 284 } else if (long.class.equals(t)) { 285 f.setLong(a, 200L); 286 expectedA.put(f.getName(), 200L); 287 f.setLong(b, 100L); 288 expectedB.put(f.getName(), 100L); 289 } else if (t.isPrimitive()) { 290 // This method doesn't yet handle other primitive types. If the relevant diff 291 // classes gain new fields of these types, please add another clause here. 292 fail("primitive type not handled by generateFieldDiffs: " + t.getName()); 293 } else if (String.class.equals(t)) { 294 f.set(a, "string1"); 295 expectedA.put(f.getName(), "string1"); 296 f.set(b, "string2"); 297 expectedB.put(f.getName(), "string2"); 298 } else { 299 // catch-all for other types: have the field be "added" 300 f.set(a, null); 301 expectedA.put(f.getName(), null); 302 try { 303 f.set(b, t.getDeclaredConstructor().newInstance()); 304 expectedB.put(f.getName(), t.getDeclaredConstructor().newInstance()); 305 } catch (Exception e) { 306 // No default constructor, or blithely attempting to construct something doesn't 307 // work for some reason. If the default value isn't null, then keep it. 308 if (f.get(b) != null) { 309 expectedB.put(f.getName(), f.get(b)); 310 } else { 311 // If we can't even rely on that, fail. Have the test-writer special case 312 // something, as this is not able to be genericized. 313 fail("could not generically construct value for field: " + f.getName()); 314 } 315 } 316 } 317 } 318 } 319 } 320