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