From 149c2dcf2d386c55f7023bf832b50e1386cf13ad Mon Sep 17 00:00:00 2001
From: Artem Zinnatullin <artem-zin@yandex-team.ru>
Date: Fri, 6 Nov 2015 17:13:17 +0300
Subject: [PATCH] StorIOContentReslver fix for observing changes of Uris on
 Android API < 16

---
 .../impl/DefaultStorIOContentResolver.java         |   3 +-
 .../contentresolver/impl/RxChangesObserver.java    |  77 +++--
 .../impl/RxChangesObserverTest.java                | 349 ++++++++++++++++-----
 .../java/com/pushtorefresh/storio/test/Utils.java  |  39 +++
 .../com/pushtorefresh/storio/test/UtilsTest.java   |  29 ++
 5 files changed, 395 insertions(+), 102 deletions(-)
 create mode 100644 storio-test-common/src/main/java/com/pushtorefresh/storio/test/Utils.java
 create mode 100644 storio-test-common/src/test/java/com/pushtorefresh/storio/test/UtilsTest.java

diff --git a/storio-content-resolver/src/main/java/com/pushtorefresh/storio/contentresolver/impl/DefaultStorIOContentResolver.java b/storio-content-resolver/src/main/java/com/pushtorefresh/storio/contentresolver/impl/DefaultStorIOContentResolver.java
index 0a629f8..f3d793e 100644
--- a/storio-content-resolver/src/main/java/com/pushtorefresh/storio/contentresolver/impl/DefaultStorIOContentResolver.java
+++ b/storio-content-resolver/src/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);
     }
 
     /**
diff --git a/storio-content-resolver/src/main/java/com/pushtorefresh/storio/contentresolver/impl/RxChangesObserver.java b/storio-content-resolver/src/main/java/com/pushtorefresh/storio/contentresolver/impl/RxChangesObserver.java
index fab71d5..a724611 100644
--- a/storio-content-resolver/src/main/java/com/pushtorefresh/storio/contentresolver/impl/RxChangesObserver.java
+++ b/storio-content-resolver/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);
+                            }
+                        }));
                     }
-                }));
+                }
             }
         });
     }
diff --git a/storio-content-resolver/src/test/java/com/pushtorefresh/storio/contentresolver/impl/RxChangesObserverTest.java b/storio-content-resolver/src/test/java/com/pushtorefresh/storio/contentresolver/impl/RxChangesObserverTest.java
index 1e8ac16..2c0092e 100644
--- a/storio-content-resolver/src/test/java/com/pushtorefresh/storio/contentresolver/impl/RxChangesObserverTest.java
+++ b/storio-content-resolver/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();
+        }
     }
 }
diff --git a/storio-test-common/src/main/java/com/pushtorefresh/storio/test/Utils.java b/storio-test-common/src/main/java/com/pushtorefresh/storio/test/Utils.java
new file mode 100644
index 0000000..30f534f
--- /dev/null
+++ b/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;
+    }
+}
diff --git a/storio-test-common/src/test/java/com/pushtorefresh/storio/test/UtilsTest.java b/storio-test-common/src/test/java/com/pushtorefresh/storio/test/UtilsTest.java
new file mode 100644
index 0000000..edb0ac0
--- /dev/null
+++ b/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);
+    }
+}
