/*
* Copyright (c) 2014 OpenSilk Productions LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.opensilk.common.core.rx;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import org.opensilk.common.core.util.Preconditions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import rx.Observable;
import rx.Scheduler;
import rx.Subscriber;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action1;
import rx.schedulers.Schedulers;
import timber.log.Timber;
/**
* Created by drew on 10/19/14.
*/
public abstract class RxCursorLoader<T> implements RxLoader<T> {
class UriObserver extends ContentObserver {
UriObserver(Handler handler) {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
reset();
for (ContentChangedListener l : contentChangedListeners) {
l.reload();
}
}
@Override
public void onChange(boolean selfChange, Uri uri) {
onChange(selfChange);
}
}
protected final List<ContentChangedListener> contentChangedListeners;
protected final Context context;
protected Scheduler subscribeScheduler = Schedulers.io();
protected Scheduler observeOnScheduler = AndroidSchedulers.mainThread();
protected Uri uri;
protected String[] projection;
protected String selection;
protected String[] selectionArgs;
protected String sortOrder;
private UriObserver uriObserver;
protected Observable<T> cachedObservable;
public RxCursorLoader(Context context) {
this(context, null, null, null, null, null);
}
public RxCursorLoader(Context context,
Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
contentChangedListeners = new ArrayList<>();
this.context = context;
this.uri = uri;
this.projection = projection;
this.selection = selection;
this.selectionArgs = selectionArgs;
this.sortOrder = sortOrder;
}
/**
* Do your thing. Any thrown exceptions will be passed to onError() unless
* using {@link #getObservable()} or {@link #getListObservable()} as they will
* capture any exception then finish.
*/
protected abstract T makeFromCursor(Cursor c) throws Exception;
/**
* @return Observable that collects all items into a List and emits that
* in a single onNext(List) call.
*/
public Observable<List<T>> getListObservable() {
return getObservable().toList();
}
/**
* Note:<br>
* The observable is cached and must be {@link #reset()} before resubscribing to requery.
* The observable does not emit errors, on error the cached observable is reset
* then calls onComplete.
*
* @return Observable
*/
public Observable<T> getObservable() {
registerContentObserver();
if (cachedObservable == null) {
cachedObservable = createObservable()
.doOnError(new Action1<Throwable>() {
@Override
public void call(Throwable throwable) {
reset();
dump(throwable);
}
})
.onErrorResumeNext(Observable.<T>empty())
.subscribeOn(subscribeScheduler)
.observeOn(observeOnScheduler)
.cache();
}
return cachedObservable;
}
/**
* @return the raw producer, suitable for chaining.
*/
public Observable<T> createObservable() {
return Observable.create(new Observable.OnSubscribe<T>() {
// Querys the mediastore
@Override
public void call(Subscriber<? super T> subscriber) {
// Timber.v("OnSubscribe %s", Thread.currentThread().getName());
Cursor c = null;
try {
if (context == null || uri == null) {
emmitError(new NullPointerException("Context and Uri must not be null"), subscriber);
return;
}
c = getCursor();
if (c == null) {
emmitError(new NullPointerException("Unable to obtain cursor"), subscriber);
return;
}
if (c.moveToFirst()) {
do {
T item = makeFromCursor(c);
if (item == null) continue; //TODO throw this?
if (subscriber.isUnsubscribed()) return;
subscriber.onNext(item);
} while (c.moveToNext());
}
if (!subscriber.isUnsubscribed()) {
subscriber.onCompleted();
}
} catch (Exception e) {
emmitError(e, subscriber);
} finally {
if (c != null) c.close();
}
}
});
}
public void reset() {
cachedObservable = null;
}
protected Cursor getCursor() {
return context.getContentResolver().query(
uri,
projection,
selection,
selectionArgs,
sortOrder
);
}
protected void registerContentObserver() {
if (uriObserver == null) {
uriObserver = new UriObserver(new Handler(Looper.getMainLooper()));
context.getContentResolver().registerContentObserver(uri, true, uriObserver);
}
}
public void addContentChangedListener(ContentChangedListener l) {
contentChangedListeners.add(l);
}
public void removeContentChangedListener(ContentChangedListener l) {
contentChangedListeners.remove(l);
}
public RxCursorLoader<T> setUri(Uri uri) {
this.uri = Preconditions.checkNotNull(uri, "Uri must not be null");
return this;
}
public RxCursorLoader<T> setProjection(String[] projection) {
this.projection = projection;
return this;
}
public RxCursorLoader<T> setSelection(String selection) {
this.selection = selection;
return this;
}
public RxCursorLoader<T> setSelectionArgs(String[] selectionArgs) {
this.selectionArgs = selectionArgs;
return this;
}
public RxCursorLoader<T> setSortOrder(String sortOrder) {
this.sortOrder = sortOrder;
return this;
}
public RxCursorLoader<T> setSubscribeScheduler(Scheduler scheduler) {
this.subscribeScheduler = Preconditions.checkNotNull(scheduler, "Scheduler must not be null");
return this;
}
public RxCursorLoader<T> setObserveOnScheduler(Scheduler scheduler) {
this.observeOnScheduler = Preconditions.checkNotNull(scheduler, "Scheduler must not be null");
return this;
}
protected void emmitError(Throwable t, Subscriber<? super T> subscriber) {
if (subscriber.isUnsubscribed()) return;
subscriber.onError(t);
dump(new Throwable(t));
}
protected void dump(Throwable throwable) {
Timber.e(throwable, "RxCursorLoader(uri=%s\nprojection=%s\nselection=%s\nselectionArgs=%s\nsortOrder=%s",
uri,
projection != null ? Arrays.toString(projection) : null,
selection,
selectionArgs != null ? Arrays.toString(selectionArgs) : null,
sortOrder);
}
}