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