Skip to content
Browse files

StorIOContentReslver fix for observing changes of Uris on Android API…

… < 16
  • Loading branch information...
1 parent a5af227 commit 149c2dcf2d386c55f7023bf832b50e1386cf13ad Artem Zinnatullin committed
View
3 ...main/java/com/pushtorefresh/storio/contentresolver/impl/DefaultStorIOContentResolver.java
@@ -5,6 +5,7 @@
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
+import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.support.annotation.NonNull;
@@ -65,7 +66,7 @@ protected DefaultStorIOContentResolver(@NonNull ContentResolver contentResolver,
// indirect usage of RxJava
// required to avoid problems with ClassLoader when RxJava is not in ClassPath
- return RxChangesObserver.observeChanges(contentResolver, uris, contentObserverHandler);
+ return RxChangesObserver.observeChanges(contentResolver, uris, contentObserverHandler, Build.VERSION.SDK_INT);
}
/**
View
77 ...solver/src/main/java/com/pushtorefresh/storio/contentresolver/impl/RxChangesObserver.java
@@ -3,6 +3,7 @@
import android.content.ContentResolver;
import android.database.ContentObserver;
import android.net.Uri;
+import android.os.Build;
import android.os.Handler;
import android.support.annotation.NonNull;
@@ -27,37 +28,67 @@ private RxChangesObserver() {
}
@NonNull
- static Observable<Changes> observeChanges(@NonNull final ContentResolver contentResolver, @NonNull final Set<Uri> uris, @NonNull final Handler handler) {
+ static Observable<Changes> observeChanges(@NonNull final ContentResolver contentResolver,
+ @NonNull final Set<Uri> uris,
+ @NonNull final Handler handler,
+ final int sdkVersion) {
return Observable.create(new Observable.OnSubscribe<Changes>() {
@Override
public void call(final Subscriber<? super Changes> subscriber) {
- final ContentObserver contentObserver = new ContentObserver(handler) {
- @Override
- public boolean deliverSelfNotifications() {
- return false;
- }
+ // Use one ContentObserver for all passed Uris on API >= 16
+ if (sdkVersion >= Build.VERSION_CODES.JELLY_BEAN) {
+ final ContentObserver contentObserver = new ContentObserver(handler) {
+ @Override
+ public boolean deliverSelfNotifications() {
+ return false;
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ subscriber.onNext(Changes.newInstance(uri));
+ }
+ };
- @Override
- public void onChange(boolean selfChange, Uri uri) {
- subscriber.onNext(Changes.newInstance(uri));
+ for (Uri uri : uris) {
+ contentResolver.registerContentObserver(
+ uri,
+ true,
+ contentObserver
+ );
}
- };
- for (Uri uri : uris) {
- contentResolver.registerContentObserver(
- uri,
- true,
- contentObserver
- );
- }
+ subscriber.add(Subscriptions.create(new Action0() {
+ @Override
+ public void call() {
+ // Prevent memory leak on unsubscribe from Observable
+ contentResolver.unregisterContentObserver(contentObserver);
+ }
+ }));
+ } else {
+ // Register separate ContentObserver for each uri on API < 16
+ for (final Uri uri : uris) {
+ final ContentObserver contentObserver = new ContentObserver(handler) {
+ @Override
+ public boolean deliverSelfNotifications() {
+ return false;
+ }
- subscriber.add(Subscriptions.create(new Action0() {
- @Override
- public void call() {
- // Preventing memory leak on unsubscribe from Observable
- contentResolver.unregisterContentObserver(contentObserver);
+ @Override
+ public void onChange(boolean selfChange) {
+ subscriber.onNext(Changes.newInstance(uri));
+ }
+ };
+
+ contentResolver.registerContentObserver(uri, true, contentObserver);
+ subscriber.add(Subscriptions.create(new Action0() {
+ @Override
+ public void call() {
+ // Prevent memory leak on unsubscribe from Observable
+ contentResolver.unregisterContentObserver(contentObserver);
+ }
+ }));
}
- }));
+ }
}
});
}
View
349 ...er/src/test/java/com/pushtorefresh/storio/contentresolver/impl/RxChangesObserverTest.java
@@ -1,34 +1,48 @@
package com.pushtorefresh.storio.contentresolver.impl;
+import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.database.ContentObserver;
import android.net.Uri;
+import android.os.Build;
import android.os.Handler;
import com.pushtorefresh.private_constructor_checker.PrivateConstructorChecker;
+import com.pushtorefresh.storio.contentresolver.BuildConfig;
import com.pushtorefresh.storio.contentresolver.Changes;
import org.junit.Test;
+import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
+import org.robolectric.RobolectricGradleTestRunner;
+import org.robolectric.annotation.Config;
+import java.util.HashMap;
import java.util.HashSet;
+import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import rx.Observable;
import rx.Subscription;
+import rx.observers.TestSubscriber;
+import static com.pushtorefresh.storio.test.Utils.MAX_SDK_VERSION;
+import static com.pushtorefresh.storio.test.Utils.MIN_SDK_VERSION;
import static java.util.Collections.singleton;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.same;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+@RunWith(RobolectricGradleTestRunner.class)
+@Config(constants = BuildConfig.class, sdk = 21)
public class RxChangesObserverTest {
@Test
@@ -41,106 +55,285 @@ public void constructorShouldBePrivateAndThrowException() {
}
@Test
- public void contentObserverShouldReturnFalseOnDeliverSelfNotifications() {
- ContentResolver contentResolver = mock(ContentResolver.class);
- Set<Uri> uris = singleton(mock(Uri.class));
+ public void contentObserverShouldReturnFalseOnDeliverSelfNotificationsOnAllSdkVersions() {
+ for (int sdkVersion = MIN_SDK_VERSION; sdkVersion < MAX_SDK_VERSION; sdkVersion++) {
+ ContentResolver contentResolver = mock(ContentResolver.class);
+ Uri uri = mock(Uri.class);
- final AtomicReference<ContentObserver> contentObserver = new AtomicReference<ContentObserver>();
-
- doAnswer(new Answer() {
- @Override
- public Object answer(InvocationOnMock invocation) throws Throwable {
- contentObserver.set((ContentObserver) invocation.getArguments()[2]);
- return null;
- }
- }).when(contentResolver)
- .registerContentObserver(any(Uri.class), eq(true), any(ContentObserver.class));
+ final AtomicReference<ContentObserver> contentObserver = new AtomicReference<ContentObserver>();
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ contentObserver.set((ContentObserver) invocation.getArguments()[2]);
+ return null;
+ }
+ }).when(contentResolver).registerContentObserver(same(uri), eq(true), any(ContentObserver.class));
- Handler handler = mock(Handler.class);
+ Handler handler = mock(Handler.class);
- Observable<Changes> observable = RxChangesObserver.observeChanges(contentResolver, uris, handler);
+ Observable<Changes> observable = RxChangesObserver.observeChanges(contentResolver, singleton(uri), handler, sdkVersion);
- Subscription subscription = observable.subscribe();
+ Subscription subscription = observable.subscribe();
- assertThat(contentObserver.get().deliverSelfNotifications()).isFalse();
+ assertThat(contentObserver.get().deliverSelfNotifications()).isFalse();
- subscription.unsubscribe();
+ subscription.unsubscribe();
+ }
}
@Test
- public void shouldRegisterContentObserverAfterSubscribingToObservable() {
- ContentResolver contentResolver = mock(ContentResolver.class);
-
- Observable<Changes> observable = RxChangesObserver
- .observeChanges(
- contentResolver,
- singleton(mock(Uri.class)),
- mock(Handler.class)
- );
-
- // Should not register ContentObserver before subscribing to Observable
- verify(contentResolver, times(0))
- .registerContentObserver(any(Uri.class), anyBoolean(), any(ContentObserver.class));
-
- Subscription subscription = observable.subscribe();
-
- // Should register Content Observer after subscribing to Observable
- verify(contentResolver)
- .registerContentObserver(any(Uri.class), anyBoolean(), any(ContentObserver.class));
+ public void shouldRegisterOnlyOneContentObserverAfterSubscribingToObservableOnSdkVersionGreaterThan15() {
+ for (int sdkVersion = 16; sdkVersion < MAX_SDK_VERSION; sdkVersion++) {
+ ContentResolver contentResolver = mock(ContentResolver.class);
+
+ final AtomicReference<ContentObserver> contentObserver = new AtomicReference<ContentObserver>();
+
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ // Save reference to ContentObserver only once to assert that it was created once
+ if (contentObserver.get() == null) {
+ contentObserver.set((ContentObserver) invocation.getArguments()[2]);
+ } else if (contentObserver.get() != invocation.getArguments()[2]) {
+ throw new AssertionError("More than one ContentObserver was created");
+ }
+ return null;
+ }
+ }).when(contentResolver).registerContentObserver(any(Uri.class), eq(true), any(ContentObserver.class));
+
+ Set<Uri> uris = new HashSet<Uri>(3);
+ uris.add(mock(Uri.class));
+ uris.add(mock(Uri.class));
+ uris.add(mock(Uri.class));
+
+ Observable<Changes> observable = RxChangesObserver
+ .observeChanges(
+ contentResolver,
+ uris,
+ mock(Handler.class),
+ sdkVersion
+ );
+
+ // Should not register ContentObserver before subscribing to Observable
+ verify(contentResolver, times(0))
+ .registerContentObserver(any(Uri.class), anyBoolean(), any(ContentObserver.class));
+
+ Subscription subscription = observable.subscribe();
+
+ for (Uri uri : uris) {
+ // Assert that same ContentObserver was registered for all uris
+ verify(contentResolver).registerContentObserver(same(uri), eq(true), same(contentObserver.get()));
+ }
- subscription.unsubscribe();
+ subscription.unsubscribe();
+ }
}
@Test
- public void shouldUnregisterContentObserverAfterUnsubscribingFromObservable() {
- ContentResolver contentResolver = mock(ContentResolver.class);
-
- Subscription subscription = RxChangesObserver
- .observeChanges(
- contentResolver,
- singleton(mock(Uri.class)),
- mock(Handler.class))
- .subscribe();
-
- // Should not unregister before unsubscribe from Subscription
- verify(contentResolver, times(0)).unregisterContentObserver(any(ContentObserver.class));
+ public void shouldRegisterObserverForEachPassedUriAfterSubscribingToObservableOnSdkVersionLowerThan15() {
+ for (int sdkVersion = MIN_SDK_VERSION; sdkVersion < 16; sdkVersion++) {
+ ContentResolver contentResolver = mock(ContentResolver.class);
+ final Map<Uri, ContentObserver> contentObservers = new HashMap<Uri, ContentObserver>(3);
+
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ contentObservers.put((Uri) invocation.getArguments()[0], (ContentObserver) invocation.getArguments()[2]);
+ return null;
+ }
+ }).when(contentResolver).registerContentObserver(any(Uri.class), eq(true), any(ContentObserver.class));
+
+ Set<Uri> uris = new HashSet<Uri>(3);
+ uris.add(mock(Uri.class));
+ uris.add(mock(Uri.class));
+ uris.add(mock(Uri.class));
+
+ Observable<Changes> observable = RxChangesObserver.observeChanges(
+ contentResolver,
+ uris,
+ mock(Handler.class),
+ sdkVersion
+ );
+
+ // Should not register ContentObserver before subscribing to Observable
+ verify(contentResolver, times(0))
+ .registerContentObserver(any(Uri.class), anyBoolean(), any(ContentObserver.class));
+
+ Subscription subscription = observable.subscribe();
+
+ for (Uri uri : uris) {
+ // Assert that new ContentObserver was registered for each uri
+ verify(contentResolver).registerContentObserver(same(uri), eq(true), same(contentObservers.get(uri)));
+ }
- subscription.unsubscribe();
+ assertThat(contentObservers).hasSameSizeAs(uris);
- // Should unregister ContentObserver after unsubscribing from Subscription
- verify(contentResolver).unregisterContentObserver(any(ContentObserver.class));
+ subscription.unsubscribe();
+ }
}
@Test
- public void shouldRegisterContentObserverForPassedUris() {
- Set<Uri> uris = new HashSet<Uri>();
-
- // All Uris are different (objects)
- uris.add(mock(Uri.class));
- uris.add(mock(Uri.class));
- uris.add(mock(Uri.class));
-
- ContentResolver contentResolver = mock(ContentResolver.class);
-
- Observable<Changes> observable = RxChangesObserver
- .observeChanges(
- contentResolver,
- uris,
- mock(Handler.class)
- );
-
- // Should not register ContentObserver before subscribing to Observable
- verify(contentResolver, times(0))
- .registerContentObserver(any(Uri.class), anyBoolean(), any(ContentObserver.class));
+ public void shouldUnregisterContentObserverAfterUnsubscribingFromObservableOnSdkVersionGreaterThan15() {
+ for (int sdkVersion = 16; sdkVersion < MAX_SDK_VERSION; sdkVersion++) {
+ ContentResolver contentResolver = mock(ContentResolver.class);
+ Set<Uri> uris = new HashSet<Uri>(3);
+ uris.add(mock(Uri.class));
+ uris.add(mock(Uri.class));
+ uris.add(mock(Uri.class));
+
+ Subscription subscription = RxChangesObserver
+ .observeChanges(
+ contentResolver,
+ uris,
+ mock(Handler.class),
+ sdkVersion)
+ .subscribe();
+
+ // Should not unregister before unsubscribe from Subscription
+ verify(contentResolver, times(0)).unregisterContentObserver(any(ContentObserver.class));
+
+ subscription.unsubscribe();
+
+ // Should unregister ContentObserver after unsubscribing from Subscription
+ verify(contentResolver).unregisterContentObserver(any(ContentObserver.class));
+ }
+ }
- Subscription subscription = observable.subscribe();
+ @Test
+ public void shouldUnregisterContentObserversForEachUriAfterUnsubscribingFromObservableOnSdkVersionLowerThan16() {
+ for (int sdkVersion = MIN_SDK_VERSION; sdkVersion < 16; sdkVersion++) {
+ ContentResolver contentResolver = mock(ContentResolver.class);
+ Set<Uri> uris = new HashSet<Uri>(3);
+ uris.add(mock(Uri.class));
+ uris.add(mock(Uri.class));
+ uris.add(mock(Uri.class));
+
+ final Map<Uri, ContentObserver> contentObservers = new HashMap<Uri, ContentObserver>(3);
+
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ contentObservers.put((Uri) invocation.getArguments()[0], (ContentObserver) invocation.getArguments()[2]);
+ return null;
+ }
+ }).when(contentResolver).registerContentObserver(any(Uri.class), eq(true), any(ContentObserver.class));
+
+ Subscription subscription = RxChangesObserver
+ .observeChanges(
+ contentResolver,
+ uris,
+ mock(Handler.class),
+ sdkVersion)
+ .subscribe();
+
+ // Should not unregister before unsubscribe from Subscription
+ verify(contentResolver, times(0)).unregisterContentObserver(any(ContentObserver.class));
+
+ subscription.unsubscribe();
+
+ for (Uri uri : uris) {
+ // Assert that ContentObserver for each uri was unregistered
+ verify(contentResolver).unregisterContentObserver(contentObservers.get(uri));
+ }
+ }
+ }
- // Should register ContentObserver for each uri from passed set of uris
- for (Uri uri : uris) {
- verify(contentResolver).registerContentObserver(eq(uri), anyBoolean(), any(ContentObserver.class));
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ @Test
+ public void shouldEmitChangesOnSdkVersionGreaterThan15() {
+ for (int sdkVersion = 16; sdkVersion < MAX_SDK_VERSION; sdkVersion++) {
+ ContentResolver contentResolver = mock(ContentResolver.class);
+ final AtomicReference<ContentObserver> contentObserver = new AtomicReference<ContentObserver>();
+
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ // Save reference to ContentObserver only once to assert that it was created once
+ if (contentObserver.get() == null) {
+ contentObserver.set((ContentObserver) invocation.getArguments()[2]);
+ } else if (contentObserver.get() != invocation.getArguments()[2]) {
+ throw new AssertionError("More than one ContentObserver was created");
+ }
+ return null;
+ }
+ }).when(contentResolver).registerContentObserver(any(Uri.class), eq(true), any(ContentObserver.class));
+
+ TestSubscriber<Changes> testSubscriber = new TestSubscriber<Changes>();
+
+ Uri uri1 = mock(Uri.class);
+ Uri uri2 = mock(Uri.class);
+ Set<Uri> uris = new HashSet<Uri>(2);
+ uris.add(uri1);
+ uris.add(uri2);
+
+ RxChangesObserver
+ .observeChanges(
+ contentResolver,
+ uris,
+ mock(Handler.class),
+ sdkVersion)
+ .subscribe(testSubscriber);
+
+ testSubscriber.assertNoTerminalEvent();
+ testSubscriber.assertNoValues();
+
+ // RxChangesObserver should ignore call to onChange() without Uri on sdkVersion >= 16
+ contentObserver.get().onChange(false);
+ testSubscriber.assertNoValues();
+
+ // Emulate change of Uris, Observable should react and emit Changes objects
+ contentObserver.get().onChange(false, uri1);
+ contentObserver.get().onChange(false, uri2);
+
+ testSubscriber.assertValues(Changes.newInstance(uri1), Changes.newInstance(uri2));
+
+ testSubscriber.unsubscribe();
+ testSubscriber.assertNoErrors();
}
+ }
- subscription.unsubscribe();
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ @Test
+ public void shouldEmitChangesOnSdkVersionLowerThan16() {
+ for (int sdkVersion = MIN_SDK_VERSION; sdkVersion < 16; sdkVersion++) {
+ ContentResolver contentResolver = mock(ContentResolver.class);
+ final Map<Uri, ContentObserver> contentObservers = new HashMap<Uri, ContentObserver>(3);
+
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ contentObservers.put((Uri) invocation.getArguments()[0], (ContentObserver) invocation.getArguments()[2]);
+ return null;
+ }
+ }).when(contentResolver).registerContentObserver(any(Uri.class), eq(true), any(ContentObserver.class));
+
+ TestSubscriber<Changes> testSubscriber = new TestSubscriber<Changes>();
+
+ Uri uri1 = mock(Uri.class);
+ Uri uri2 = mock(Uri.class);
+ Set<Uri> uris = new HashSet<Uri>(2);
+ uris.add(uri1);
+ uris.add(uri2);
+
+ RxChangesObserver
+ .observeChanges(
+ contentResolver,
+ uris,
+ mock(Handler.class),
+ sdkVersion)
+ .subscribe(testSubscriber);
+
+ testSubscriber.assertNoTerminalEvent();
+ testSubscriber.assertNoValues();
+
+ // Emulate change of Uris, Observable should react and emit Changes objects
+ contentObservers.get(uri1).onChange(false);
+ contentObservers.get(uri2).onChange(false);
+ testSubscriber.assertValues(Changes.newInstance(uri1), Changes.newInstance(uri2));
+
+ testSubscriber.unsubscribe();
+ testSubscriber.assertNoErrors();
+ }
}
}
View
39 storio-test-common/src/main/java/com/pushtorefresh/storio/test/Utils.java
@@ -0,0 +1,39 @@
+package com.pushtorefresh.storio.test;
+
+import android.os.Build;
+
+import java.lang.reflect.Field;
+
+public class Utils {
+
+ public static final int MAX_SDK_VERSION = getMaxSdkVersion();
+ public static final int MIN_SDK_VERSION = 14;
+
+ private Utils() {
+ throw new IllegalStateException("No instances please!");
+ }
+
+ private static int getMaxSdkVersion() {
+ final Field[] versionCodesFields = Build.VERSION_CODES.class.getDeclaredFields();
+
+ // At least 23
+ int maxSdkVersion = 23;
+
+ for (final Field versionCodeField : versionCodesFields) {
+ versionCodeField.setAccessible(true);
+
+ try {
+ final Class<?> fieldType = versionCodeField.getType();
+
+ if (fieldType.equals(Integer.class)) {
+ int sdkVersion = (Integer) versionCodeField.get(null);
+ maxSdkVersion = Math.max(maxSdkVersion, sdkVersion);
+ }
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ return maxSdkVersion;
+ }
+}
View
29 storio-test-common/src/test/java/com/pushtorefresh/storio/test/UtilsTest.java
@@ -0,0 +1,29 @@
+package com.pushtorefresh.storio.test;
+
+import com.pushtorefresh.private_constructor_checker.PrivateConstructorChecker;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class UtilsTest {
+
+ @Test
+ public void constructorShouldBePrivateAndThrowException() {
+ PrivateConstructorChecker
+ .forClass(Utils.class)
+ .expectedTypeOfException(IllegalStateException.class)
+ .expectedExceptionMessage("No instances please!")
+ .check();
+ }
+
+ @Test
+ public void maxSdkVersionShouldBeAtLeast23() {
+ assertThat(Utils.MAX_SDK_VERSION).isGreaterThanOrEqualTo(23);
+ }
+
+ @Test
+ public void minSdkVersionShouldBe14() {
+ assertThat(Utils.MIN_SDK_VERSION).isEqualTo(14);
+ }
+}

0 comments on commit 149c2dc

Please sign in to comment.
Something went wrong with that request. Please try again.