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