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