/*
* Copyright 2016 Substance Mobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.animbus.music.tasks;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.annotation.WorkerThread;
import android.util.Log;
import com.animbus.music.media.objects.MediaObject;
import java.util.ArrayList;
import java.util.List;
/**
* A class that needs to be extended in order to load a list of a certain type of object.
* @param <Return> The type to return when done loading. This type should <b>NOT</b> be a {@link List}
*/
public abstract class Loader<Return extends MediaObject> {
protected Context context;
public Loader(Context context, Object... params) {
this.context = context.getApplicationContext();
this.runParams = params;
mObserver = getObserver();
}
@WorkerThread
@Nullable
protected abstract Return buildObject(@NonNull Cursor cursor);
///////////////////////////////////////////////////////////////////////////
// Used for generating the Cursor
///////////////////////////////////////////////////////////////////////////
protected abstract Uri getUri();
protected String[] getProjection() {
return null;
}
protected String getSelection() {
return null;
}
protected String[] getSelectionArgs() {
return null;
}
protected String getSortOrder() {
return null;
}
///////////////////////////////////////////////////////////////////////////
// Underlying AsyncTask
///////////////////////////////////////////////////////////////////////////
private LoadTask mTask;
class LoadTask extends AsyncTask<Object, Return, List<Return>> {
private boolean isExecuting = false;
@Override
protected void onPreExecute() {
super.onPreExecute();
unregisterMediaStoreListenerTemporarily();
}
@Override
protected List<Return> doInBackground(Object... params) {
isExecuting = true;
Cursor cursor = getContext().getContentResolver().query(getUri(), getProjection(), getSelection(), getSelectionArgs(), getSortOrder());
try {
//If there is no data return an empty list
if (cursor == null || !cursor.moveToFirst()) return new ArrayList<>();
//If there is data then continue
List<Return> generated = new ArrayList<>();
do {
Return obj = buildObject(cursor);
if (obj != null) {
obj.setPosInList(cursor.getPosition())
.setContext(getContext())
.lock();
generated.add(obj);
notifyOneLoaded(obj);
}
} while (cursor.moveToNext() && !cursor.isClosed() && !isCancelled());
return generated;
} finally {
if (cursor != null && !cursor.isClosed()) cursor.close();
isExecuting = false;
}
}
@SafeVarargs
@WorkerThread
public final void oneLoaded(Return... progress) {
publishProgress(progress);
}
@SafeVarargs
@Override
protected final void onProgressUpdate(Return... values) {
super.onProgressUpdate(values);
for (Return val : values) mVerifyListener.onOneLoaded(val, val.getPosInList());
}
@Override
protected void onPostExecute(List<Return> result) {
super.onPostExecute(result);
sort(result);
mVerifyListener.onCompleted(result);
if (!mObserverLock) registerMediaStoreListener();
if (mUpdateQueued) update(result);
}
public boolean isExecuting() {
return isExecuting;
}
}
///////////////////////////////////////////////////////////////////////////
// Run
///////////////////////////////////////////////////////////////////////////
protected Object[] runParams;
@UiThread
public void run() {
if (mTask == null || mTask.getStatus() == AsyncTask.Status.FINISHED) mTask = new LoadTask();
mTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, runParams);
}
///////////////////////////////////////////////////////////////////////////
// Sorting
///////////////////////////////////////////////////////////////////////////
@UiThread
protected void sort(List<Return> data) {
//Do nothing
}
///////////////////////////////////////////////////////////////////////////
// Update
///////////////////////////////////////////////////////////////////////////
//Currently loaded data. When calling update() this will be set, then emptied
private List<Return> currentData = new ArrayList<>();
private TaskListener<Return> mVerifyListener = new TaskListener<Return>() {
@Override
public void onOneLoaded(Return item, int pos) {
/*if (!currentData.contains(item)) */for (TaskListener<Return> listener : mListeners) listener.onOneLoaded(item, pos);
}
@Override
public void onCompleted(List<Return> result) {
if (currentData != result) for (TaskListener<Return> listener : mListeners) listener.onCompleted(result);
currentData.clear();
}
};
private boolean mUpdateQueued = false;
protected final ContentObserver mObserver;
private boolean mObserverLock = false;
/**
* Make a content observer to be registered/unregistered. Do <b>NOT</b> pass variables. Whatever is returned here will be set in a final variable and accessed from there
* @return The content observer
*/
@Nullable
@UiThread
protected ContentObserver getObserver() {
return null;
}
@UiThread
public void update(final List<Return> currentData) {
if (mTask != null && !mTask.isExecuting() && currentData != null) {
mUpdateQueued = false;
this.currentData = currentData;
run();
} else {
Log.e(getClass().getSimpleName(), "Update: FAILED");
mUpdateQueued = true;
}
}
/**
* Registers a {@link ContentObserver} and removes any previous calls to {@link #unregisterMediaStoreListener()}
*/
@UiThread
public void registerMediaStoreListener() {
if (mObserver != null) {
getContext().getContentResolver().registerContentObserver(getUri(), true, mObserver);
mObserverLock = false;
}
}
/**
* Unregisters a content observer until the async task will register it again when it completes.
*/
@UiThread
protected void unregisterMediaStoreListenerTemporarily() {
if (mObserver != null) {
getContext().getContentResolver().unregisterContentObserver(mObserver);
}
}
/**
* Same as {@link #unregisterMediaStoreListenerTemporarily()} except it also makes sure the async task doesn't ever register the listener again until {@link #registerMediaStoreListener()} is called from an outside source
*/
@UiThread
public void unregisterMediaStoreListener() {
if (mObserver != null) {
unregisterMediaStoreListenerTemporarily();
mObserverLock = true;
}
}
///////////////////////////////////////////////////////////////////////////
// Listener
///////////////////////////////////////////////////////////////////////////
/**
* The listener for {@link Loader} events
* @param <Return> What type of variable should be passed to the listener. When extending {@link Loader}, you will specify what this should be
*/
public interface TaskListener<Return> {
void onOneLoaded(Return item, int pos);
void onCompleted(List<Return> result);
}
protected List<TaskListener<Return>> mListeners = new ArrayList<>();
@UiThread
public void addListener(TaskListener<Return> listener) {
mListeners.add(listener);
}
///////////////////////////////////////////////////////////////////////////
// Misc.
///////////////////////////////////////////////////////////////////////////
/**
* Wrapper method around {@link AsyncTask#publishProgress(Object[])}
* @param progress What to pass to the AsyncTask
*/
@WorkerThread
public void notifyOneLoaded(Return progress){
mTask.oneLoaded(progress);
}
/**
* @return Application congress taken from provided congress
*/
public Context getContext() {
return context;
}
}