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