• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 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.provider.cts.settings;
18 
19 import static android.provider.DeviceConfig.SYNC_DISABLED_MODE_NONE;
20 import static android.provider.Settings.RESET_MODE_PACKAGE_DEFAULTS;
21 
22 import static org.junit.Assert.assertEquals;
23 import static org.junit.Assert.assertFalse;
24 import static org.junit.Assert.assertNull;
25 import static org.junit.Assert.assertTrue;
26 import static org.junit.Assert.fail;
27 
28 import android.annotation.NonNull;
29 import android.annotation.UserIdInt;
30 import android.content.ContentResolver;
31 import android.content.Context;
32 import android.database.ContentObserver;
33 import android.net.Uri;
34 import android.os.UserHandle;
35 import android.provider.DeviceConfig;
36 import android.provider.Settings;
37 import android.util.Log;
38 
39 import androidx.test.InstrumentationRegistry;
40 import androidx.test.runner.AndroidJUnit4;
41 
42 import com.android.compatibility.common.util.PollingCheck;
43 import com.android.internal.annotations.GuardedBy;
44 
45 import org.junit.After;
46 import org.junit.Before;
47 import org.junit.Test;
48 import org.junit.runner.RunWith;
49 
50 import java.util.ArrayList;
51 import java.util.Arrays;
52 import java.util.Collection;
53 import java.util.HashMap;
54 import java.util.HashSet;
55 import java.util.List;
56 import java.util.Map;
57 import java.util.Objects;
58 import java.util.Set;
59 import java.util.concurrent.CountDownLatch;
60 import java.util.concurrent.Executors;
61 import java.util.concurrent.TimeUnit;
62 
63 @RunWith(AndroidJUnit4.class)
64 public class Settings_ConfigTest {
65 
66     private static final String NAMESPACE1 = "namespace1";
67     private static final String NAMESPACE2 = "namespace2";
68     private static final String EMPTY_NAMESPACE = "empty_namespace";
69     private static final String KEY1 = "key1";
70     private static final String KEY2 = "key2";
71     private static final String VALUE1 = "value1";
72     private static final String VALUE2 = "value2";
73     private static final String VALUE3 = "value3";
74     private static final String VALUE4 = "value4";
75     private static final String DEFAULT_VALUE = "default_value";
76 
77 
78     private static final String TAG = "ContentResolverTest";
79 
80     private static final Uri TABLE1_URI = Uri.parse("content://"
81                                             + Settings.AUTHORITY + "/config");
82 
83     private static final String TEST_PACKAGE_NAME = "android.content.cts";
84 
85     private static final long OPERATION_TIMEOUT_MS = 5000;
86 
87     private static final Context CONTEXT = InstrumentationRegistry.getContext();
88 
89 
90     private static final long WAIT_FOR_PROPERTY_CHANGE_TIMEOUT_MILLIS = 2000; // 2 sec
91     private final Object mLock = new Object();
92 
93 
94     private static final String WRITE_DEVICE_CONFIG_PERMISSION =
95             "android.permission.WRITE_DEVICE_CONFIG";
96 
97     private static final String READ_DEVICE_CONFIG_PERMISSION =
98             "android.permission.READ_DEVICE_CONFIG";
99 
100     private static final String MONITOR_DEVICE_CONFIG_ACCESS =
101             "android.permission.MONITOR_DEVICE_CONFIG_ACCESS";
102 
103     private static ContentResolver sContentResolver;
104     private static Context sContext;
105 
106     /**
107      * Get necessary permissions to access Setting.Config API and set up context
108      */
109     @Before
setUpContext()110     public void setUpContext() throws Exception {
111         InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
112                 WRITE_DEVICE_CONFIG_PERMISSION, READ_DEVICE_CONFIG_PERMISSION,
113                 MONITOR_DEVICE_CONFIG_ACCESS);
114         sContext = InstrumentationRegistry.getContext();
115         sContentResolver = sContext.getContentResolver();
116     }
117 
118     /**
119      * Clean up the namespaces, sync mode and permissions.
120      */
121     @After
cleanUp()122     public void cleanUp() throws Exception {
123         Settings.Config.setSyncDisabledMode(SYNC_DISABLED_MODE_NONE);
124         deleteProperties(NAMESPACE1, Arrays.asList(KEY1, KEY2));
125         deleteProperties(NAMESPACE2, Arrays.asList(KEY1, KEY2));
126         InstrumentationRegistry.getInstrumentation().getUiAutomation()
127                 .dropShellPermissionIdentity();
128     }
129 
130      /**
131      * Checks that getting string which does not exist returns null.
132      */
133     @Test
testGetString_empty()134     public void testGetString_empty() {
135         String result = Settings.Config.getString(KEY1);
136         assertNull("Request for non existent flag name in Settings.Config API should return null "
137                 + "while " + result + " was returned", result);
138     }
139 
140     /**
141      * Checks that getting strings which does not exist returns empty map.
142      */
143     @Test
testGetStrings_empty()144     public void testGetStrings_empty() {
145         Map<String, String> result = Settings.Config
146                 .getStrings(EMPTY_NAMESPACE, Arrays.asList(KEY1));
147         assertTrue("Request for non existent flag name in Settings.Config API should return "
148                 + "empty map while " + result.toString() + " was returned", result.isEmpty());
149     }
150 
151     /**
152      * Checks that setting and getting string from the same namespace return correct value.
153      */
154     @Test
testSetAndGetString_sameNamespace()155     public void testSetAndGetString_sameNamespace() {
156         Settings.Config.putString(NAMESPACE1, KEY1, VALUE1, /*makeDefault=*/false);
157         String result = Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY1)).get(KEY1);
158         assertEquals("Value read from Settings.Config API does not match written value.", VALUE1,
159                 result);
160     }
161 
162     /**
163      * Checks that setting a string in one namespace does not set the same string in a different
164      * namespace.
165      */
166     @Test
testSetAndGetString_differentNamespace()167     public void testSetAndGetString_differentNamespace() {
168         Settings.Config.putString(NAMESPACE1, KEY1, VALUE1, /*makeDefault=*/false);
169         String result = Settings.Config.getStrings(NAMESPACE2, Arrays.asList(KEY1)).get(KEY1);
170         assertNull("Value for same keys written to different namespaces must not clash", result);
171     }
172 
173     /**
174      * Checks that different namespaces can keep different values for the same key.
175      */
176     @Test
testSetAndGetString_multipleNamespaces()177     public void testSetAndGetString_multipleNamespaces() {
178         Settings.Config.putString(NAMESPACE1, KEY1, VALUE1, /*makeDefault=*/false);
179         Settings.Config.putString(NAMESPACE2, KEY1, VALUE2, /*makeDefault=*/false);
180         String result = Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY1)).get(KEY1);
181         assertEquals("Value read from Settings.Config  API does not match written value.", VALUE1,
182                 result);
183         result = Settings.Config.getStrings(NAMESPACE2, Arrays.asList(KEY1)).get(KEY1);
184         assertEquals("Value read from Settings.Config API does not match written value.", VALUE2,
185                 result);
186     }
187 
188     /**
189      * Checks that saving value twice keeps the last value.
190      */
191     @Test
testSetAndGetString_overrideValue()192     public void testSetAndGetString_overrideValue() {
193         Settings.Config.putString(NAMESPACE1, KEY1, VALUE1, /*makeDefault=*/false);
194         Settings.Config.putString(NAMESPACE1, KEY1, VALUE2, /*makeDefault=*/false);
195         String result = Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY1)).get(KEY1);
196         assertEquals("New value written to the same namespace/key did not override previous"
197                 + " value.", VALUE2, result);
198     }
199 
200     /**
201      * Checks that putString() fails with NullPointerException when called with null namespace.
202      */
203     @Test
testPutString_nullNamespace()204     public void testPutString_nullNamespace() {
205         try {
206             Settings.Config.putString(null, KEY1, DEFAULT_VALUE, /*makeDefault=*/false);
207             fail("Settings.Config.putString() with null namespace must result in "
208                     + "NullPointerException");
209         } catch (NullPointerException e) {
210             // expected
211         }
212     }
213 
214     /**
215      * Checks that putString() fails with NullPointerException when called with null name.
216      */
217     @Test
testPutString_nullName()218     public void testPutString_nullName() {
219         try {
220             Settings.Config.putString(NAMESPACE1, null, DEFAULT_VALUE, /*makeDefault=*/false);
221             fail("Settings.Config.putString() with null name must result in NullPointerException");
222         } catch (NullPointerException e) {
223             // expected
224         }
225     }
226 
227     /**
228      * Checks that setting and getting strings from the same namespace return correct values.
229      */
230     @Test
testSetAndGetStrings_sameNamespace()231     public void testSetAndGetStrings_sameNamespace() throws Exception {
232         assertNull(Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY1)).get(KEY1));
233         assertNull(Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY1)).get(KEY2));
234         Settings.Config.setStrings(NAMESPACE1, new HashMap<String, String>() {{
235                 put(KEY1, VALUE1);
236                 put(KEY2, VALUE2);
237             }});
238 
239         assertEquals(VALUE1, Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY1)).get(KEY1));
240         assertEquals(VALUE2, Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY2)).get(KEY2));
241     }
242 
243     /**
244      * Checks that setting strings in one namespace does not set the same strings in a
245      * different namespace.
246      */
247     @Test
testSetAndGetStrings_differentNamespace()248     public void testSetAndGetStrings_differentNamespace() throws Exception {
249         Settings.Config.setStrings(NAMESPACE1, new HashMap<String, String>() {{
250                 put(KEY1, VALUE1);
251                 put(KEY2, VALUE2);
252             }});
253 
254         assertNull(Settings.Config.getStrings(NAMESPACE2, Arrays.asList(KEY1)).get(KEY1));
255         assertNull(Settings.Config.getStrings(NAMESPACE2, Arrays.asList(KEY2)).get(KEY2));
256     }
257 
258     /**
259      * Checks that different namespaces can keep different values for the same keys.
260      */
261     @Test
testSetAndGetStrings_multipleNamespaces()262     public void testSetAndGetStrings_multipleNamespaces() throws Exception {
263         Settings.Config.setStrings(NAMESPACE1, new HashMap<String, String>() {{
264                 put(KEY1, VALUE1);
265                 put(KEY2, VALUE2);
266             }});
267         Settings.Config.setStrings(NAMESPACE2, new HashMap<String, String>() {{
268                 put(KEY1, VALUE3);
269                 put(KEY2, VALUE4);
270             }});
271 
272         Map<String, String> namespace1Values = Settings.Config
273                 .getStrings(NAMESPACE1, Arrays.asList(KEY1, KEY2));
274         Map<String, String> namespace2Values = Settings.Config
275                 .getStrings(NAMESPACE2, Arrays.asList(KEY1, KEY2));
276 
277         assertEquals(namespace1Values.toString(), VALUE1, namespace1Values.get(KEY1));
278         assertEquals(namespace1Values.toString(), VALUE2, namespace1Values.get(KEY2));
279         assertEquals(namespace2Values.toString(), VALUE3, namespace2Values.get(KEY1));
280         assertEquals(namespace2Values.toString(), VALUE4, namespace2Values.get(KEY2));
281     }
282 
283 
284     /**
285      * Checks that saving values twice keeps the last values.
286      */
287     @Test
testSetAndGetStrings_overrideValue()288     public void testSetAndGetStrings_overrideValue() throws Exception {
289         Settings.Config.setStrings(NAMESPACE1, new HashMap<String, String>() {{
290                 put(KEY1, VALUE1);
291                 put(KEY2, VALUE2);
292             }});
293 
294         Settings.Config.setStrings(NAMESPACE1, new HashMap<String, String>() {{
295                 put(KEY1, VALUE3);
296                 put(KEY2, VALUE4);
297             }});
298 
299         assertEquals(VALUE3, Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY1)).get(KEY1));
300         assertEquals(VALUE4, Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY2)).get(KEY2));
301     }
302 
303 
304     /**
305      * Checks that deleteString() fails with NullPointerException when called with null namespace.
306      */
307     @Test
testDeleteString_nullKey()308     public void testDeleteString_nullKey() {
309         try {
310             Settings.Config.deleteString(null, KEY1);
311             fail("Settings.Config.deleteString() with null namespace must result in "
312                     + "NullPointerException");
313         } catch (NullPointerException e) {
314             // expected
315         }
316     }
317 
318     /**
319      * Checks that deleteString() fails with NullPointerException when called with null key.
320      */
321     @Test
testDeleteString_nullNamespace()322     public void testDeleteString_nullNamespace() {
323         try {
324             Settings.Config.deleteString(NAMESPACE1, null);
325             fail("Settings.Config.deleteString() with null key must result in "
326                     + "NullPointerException");
327         } catch (NullPointerException e) {
328             // expected
329         }
330     }
331 
332     /**
333      * Checks delete string.
334      */
335     @Test
testDeleteString()336     public void testDeleteString() {
337         Settings.Config.putString(NAMESPACE1, KEY1, VALUE1, /*makeDefault=*/false);
338         assertEquals(VALUE1, Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY1)).get(KEY1));
339 
340         Settings.Config.deleteString(NAMESPACE1, KEY1);
341         assertNull(Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY1)).get(KEY1));
342     }
343 
344 
345     /**
346      * Test that reset to package default successfully resets values.
347      */
348     @Test
testResetToPackageDefaults()349     public void testResetToPackageDefaults() {
350         Settings.Config.putString(NAMESPACE1, KEY1, VALUE1, /*makeDefault=*/true);
351         Settings.Config.putString(NAMESPACE1, KEY1, VALUE2, /*makeDefault=*/false);
352 
353         assertEquals(VALUE2, Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY1)).get(KEY1));
354 
355         Settings.Config.resetToDefaults(RESET_MODE_PACKAGE_DEFAULTS, NAMESPACE1);
356 
357         assertEquals(VALUE1, Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY1)).get(KEY1));
358     }
359 
360     /**
361      * Test updating syncDisabledMode.
362      */
363     @Test
testSetSyncDisabledMode()364     public void testSetSyncDisabledMode() {
365         Settings.Config.setSyncDisabledMode(SYNC_DISABLED_MODE_NONE);
366         assertEquals(SYNC_DISABLED_MODE_NONE, Settings.Config.getSyncDisabledMode());
367         Settings.Config.setSyncDisabledMode(RESET_MODE_PACKAGE_DEFAULTS);
368         assertEquals(RESET_MODE_PACKAGE_DEFAULTS, Settings.Config.getSyncDisabledMode());
369     }
370 
371     /**
372      * Test register content observer.
373      */
374     @Test
testRegisterContentObserver()375     public void testRegisterContentObserver() {
376         final MockContentObserver mco = new MockContentObserver();
377 
378         Settings.Config.registerContentObserver(NAMESPACE1, true, mco);
379         assertFalse(mco.hadOnChanged());
380 
381         Settings.Config.putString(NAMESPACE1, KEY1, VALUE2, /*makeDefault=*/false);
382         new PollingCheck() {
383             @Override
384             protected boolean check() {
385                 return mco.hadOnChanged();
386             }
387         }.run();
388 
389         mco.reset();
390         Settings.Config.unregisterContentObserver(mco);
391         assertFalse(mco.hadOnChanged());
392         Settings.Config.putString(NAMESPACE1, KEY1, VALUE1, /*makeDefault=*/false);
393 
394         assertFalse(mco.hadOnChanged());
395 
396         try {
397             Settings.Config.registerContentObserver(null, false, mco);
398             fail("did not throw Exceptionwhen uri is null.");
399         } catch (NullPointerException e) {
400             //expected.
401         } catch (IllegalArgumentException e) {
402             // also expected
403         }
404 
405         try {
406             Settings.Config.registerContentObserver(NAMESPACE1, false, null);
407             fail("did not throw Exception when register null content observer.");
408         } catch (NullPointerException e) {
409             //expected.
410         }
411 
412         try {
413             sContentResolver.unregisterContentObserver(null);
414             fail("did not throw NullPointerException when unregister null content observer.");
415         } catch (NullPointerException e) {
416             //expected.
417         }
418     }
419 
420     /**
421      * Test set monitor callback.
422      */
423     @Test
testSetMonitorCallback()424     public void testSetMonitorCallback() {
425         final CountDownLatch latch = new CountDownLatch(2);
426         final TestMonitorCallback callback = new TestMonitorCallback(latch);
427 
428         Settings.Config.setMonitorCallback(sContentResolver,
429                 Executors.newSingleThreadExecutor(), callback);
430         try {
431             Settings.Config.setStrings(NAMESPACE1, new HashMap<String, String>() {{
432                     put(KEY1, VALUE1);
433                     put(KEY2, VALUE2);
434                 }});
435         } catch (DeviceConfig.BadConfigException e) {
436             fail("Callback set strings" + e.toString());
437         }
438         // Reading properties triggers the monitor callback function.
439         Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY1));
440 
441         try {
442             if (!latch.await(OPERATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
443                 fail("Callback function was not called");
444             }
445         } catch (InterruptedException e) {
446             // this part is executed when an exception (in this example InterruptedException) occurs
447             fail("Callback function was not called due to interruption" + e.toString());
448         }
449         assertEquals(callback.onNamespaceUpdateCalls, 1);
450         assertEquals(callback.onDeviceConfigAccessCalls, 1);
451     }
452 
453     /**
454      * Test clear monitor callback.
455      */
456     @Test
testClearMonitorCallback()457     public void testClearMonitorCallback() {
458         final CountDownLatch latch = new CountDownLatch(2);
459         final TestMonitorCallback callback = new TestMonitorCallback(latch);
460 
461         Settings.Config.setMonitorCallback(sContentResolver,
462                 Executors.newSingleThreadExecutor(), callback);
463         Settings.Config.clearMonitorCallback(sContentResolver);
464         // Reading properties triggers the monitor callback function.
465         Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY1));
466         try {
467             Settings.Config.setStrings(NAMESPACE1, new HashMap<String, String>() {{
468                     put(KEY1, VALUE1);
469                     put(KEY2, VALUE2);
470                 }});
471         } catch (DeviceConfig.BadConfigException e) {
472             fail("Callback set strings" + e.toString());
473         }
474 
475         try {
476             if (latch.await(OPERATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
477                 fail("Callback function was called while it has been cleared");
478             }
479         } catch (InterruptedException e) {
480             // this part is executed when an exception (in this example InterruptedException) occurs
481             fail("un expected interruption occur" + e.toString());
482         }
483         assertEquals(callback.onNamespaceUpdateCalls, 0);
484         assertEquals(callback.onDeviceConfigAccessCalls, 0);
485     }
486 
487     private class TestMonitorCallback implements DeviceConfig.MonitorCallback {
488         public int onNamespaceUpdateCalls = 0;
489         public int onDeviceConfigAccessCalls = 0;
490         public CountDownLatch latch;
491 
TestMonitorCallback(CountDownLatch latch)492         TestMonitorCallback(CountDownLatch latch) {
493             this.latch = latch;
494         }
495 
onNamespaceUpdate(@onNull String updatedNamespace)496         public void onNamespaceUpdate(@NonNull String updatedNamespace) {
497             onNamespaceUpdateCalls++;
498             latch.countDown();
499         }
500 
onDeviceConfigAccess(@onNull String callingPackage, @NonNull String namespace)501         public void onDeviceConfigAccess(@NonNull String callingPackage,
502                 @NonNull String namespace) {
503             onDeviceConfigAccessCalls++;
504             latch.countDown();
505         }
506     }
507 
deleteProperty(String namespace, String key)508     private static void deleteProperty(String namespace, String key) {
509         Settings.Config.deleteString(namespace, key);
510     }
511 
deleteProperties(String namespace, List<String> keys)512     private static void deleteProperties(String namespace, List<String> keys) {
513         HashMap<String, String> deletedKeys = new HashMap<String, String>();
514         for (String key : keys) {
515             deletedKeys.put(key, null);
516         }
517 
518         try {
519             Settings.Config.setStrings(namespace, deletedKeys);
520         } catch (DeviceConfig.BadConfigException e) {
521             fail("Failed to delete the properties " + e.toString());
522         }
523     }
524 
525     private static class MockContentObserver extends ContentObserver {
526         private boolean mHadOnChanged = false;
527         private List<Change> mChanges = new ArrayList<>();
528 
MockContentObserver()529         MockContentObserver() {
530             super(null);
531         }
532 
533         @Override
deliverSelfNotifications()534         public boolean deliverSelfNotifications() {
535             return true;
536         }
537 
538         @Override
onChange(boolean selfChange, Collection<Uri> uris, int flags)539         public synchronized void onChange(boolean selfChange, Collection<Uri> uris, int flags) {
540             doOnChangeLocked(selfChange, uris, flags, /*userId=*/ -1);
541         }
542 
543         @Override
onChange(boolean selfChange, @NonNull Collection<Uri> uris, @ContentResolver.NotifyFlags int flags, UserHandle user)544         public synchronized void onChange(boolean selfChange, @NonNull Collection<Uri> uris,
545                 @ContentResolver.NotifyFlags int flags, UserHandle user) {
546             doOnChangeLocked(selfChange, uris, flags, user.getIdentifier());
547         }
548 
hadOnChanged()549         public synchronized boolean hadOnChanged() {
550             return mHadOnChanged;
551         }
552 
reset()553         public synchronized void reset() {
554             mHadOnChanged = false;
555         }
556 
hadChanges(Collection<Change> changes)557         public synchronized boolean hadChanges(Collection<Change> changes) {
558             return mChanges.containsAll(changes);
559         }
560 
561         @GuardedBy("this")
doOnChangeLocked(boolean selfChange, @NonNull Collection<Uri> uris, @ContentResolver.NotifyFlags int flags, @UserIdInt int userId)562         private void doOnChangeLocked(boolean selfChange, @NonNull Collection<Uri> uris,
563                 @ContentResolver.NotifyFlags int flags, @UserIdInt int userId) {
564             final Change change = new Change(selfChange, uris, flags, userId);
565             Log.v(TAG, change.toString());
566 
567             mHadOnChanged = true;
568             mChanges.add(change);
569         }
570     }
571 
572     public static class Change {
573         public final boolean selfChange;
574         public final Iterable<Uri> uris;
575         public final int flags;
576         @UserIdInt
577         public final int userId;
578 
Change(boolean selfChange, Iterable<Uri> uris, int flags)579         public Change(boolean selfChange, Iterable<Uri> uris, int flags) {
580             this.selfChange = selfChange;
581             this.uris = uris;
582             this.flags = flags;
583             this.userId = -1;
584         }
585 
Change(boolean selfChange, Iterable<Uri> uris, int flags, @UserIdInt int userId)586         public Change(boolean selfChange, Iterable<Uri> uris, int flags, @UserIdInt int userId) {
587             this.selfChange = selfChange;
588             this.uris = uris;
589             this.flags = flags;
590             this.userId = userId;
591         }
592 
593         @Override
toString()594         public String toString() {
595             return String.format("onChange(%b, %s, %d, %d)",
596                     selfChange, asSet(uris).toString(), flags, userId);
597         }
598 
599         @Override
equals(Object other)600         public boolean equals(Object other) {
601             if (other instanceof Change) {
602                 final Change change = (Change) other;
603                 return change.selfChange == selfChange
604                         && Objects.equals(asSet(change.uris), asSet(uris))
605                         && change.flags == flags
606                         && change.userId == userId;
607             } else {
608                 return false;
609             }
610         }
611 
asSet(Iterable<Uri> uris)612         private static Set<Uri> asSet(Iterable<Uri> uris) {
613             final Set<Uri> asSet = new HashSet<>();
614             uris.forEach(asSet::add);
615             return asSet;
616         }
617     }
618 }
619