/*
* Copyright (C) 2014 AChep@xda <artemchep@gmail.com>
*
* 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 2
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package com.achep.base.async;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.achep.base.AppHeap;
import com.achep.base.interfaces.IOnLowMemory;
import com.achep.base.utils.IOUtils;
import com.achep.base.utils.NetworkUtils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import timber.log.Timber;
/**
* A better {@link AsyncTask}.
*
* @author Artem Chepurnoy
*/
public abstract class AsyncTask<A, B, C> extends android.os.AsyncTask<A, B, C> {
public static void stop(@Nullable AsyncTask asyncTask) {
if (asyncTask != null && !asyncTask.isFinished()) {
asyncTask.cancel();
}
}
public static void stop(@Nullable android.os.AsyncTask asyncTask) {
if (asyncTask != null && !asyncTask.getStatus().equals(Status.FINISHED)) {
asyncTask.cancel(false);
}
}
/**
* Equals to calling: {@code AsyncTask.getStatus().equals(AsyncTask.Status.FINISHED)}
*/
public boolean isFinished() {
return getStatus().equals(Status.FINISHED);
}
/**
* Equals to calling: {@code AsyncTask.cancel(false)}
*/
public void cancel() {
cancel(false);
}
/**
* Downloads text files from internet. Note, that forcing task to stop immediately
* will likely produce a memory leak.
*
* @author Artem Chepurnoy
*/
public static class DownloadText extends AsyncTask<String, Void, String[]> implements IOnLowMemory {
private static final String TAG = "DownloadText";
private static final int MAX_THREAD_NUM = 5;
private final WeakReference<Callback> mCallback;
private final ConcurrentHashMap<String, String> mMap;
private final List<LoaderThread> mThreadList;
private boolean mReduceThreads = false;
/**
* Reduces the number of running download tasks to one.
* This is not possible to revert.
*/
@Override
public void onLowMemory() {
mReduceThreads = true;
}
/**
* Interface definition for a callback to be invoked
* when downloading finished or failed.
*/
public interface Callback {
/**
* Called when downloading finished or failed.
*/
void onDownloaded(@NonNull String[] texts);
}
private static class LoaderThread extends Thread {
private final ConcurrentHashMap<String, String> mMap;
private final String mUrl;
public LoaderThread(ConcurrentHashMap<String, String> map, String url) {
mMap = map;
mUrl = url;
}
@Override
// TODO: Calculate how much downloading will take
// to be able to kick threads effectively.
public void run() {
Timber.tag(TAG).d("Fetching from " + mUrl);
InputStream is = null;
InputStreamReader isr = null;
BufferedReader br = null;
try {
is = new URL(mUrl).openStream();
isr = new InputStreamReader(is);
br = new BufferedReader(isr);
String result = IOUtils.readTextFromBufferedReader(br);
mMap.put(mUrl, result);
Timber.tag(TAG).d("Done fetching from " + mUrl);
} catch (IOException e) {
Timber.tag(TAG).d("Failed fetching from " + mUrl);
} finally {
try {
if (br != null) {
br.close();
} else if (isr != null) {
isr.close();
} else if (is != null) {
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public DownloadText(@NonNull Callback callback) {
mCallback = new WeakReference<>(callback);
int initialCapacity = Math.min(MAX_THREAD_NUM, 5);
mMap = new ConcurrentHashMap<>(initialCapacity);
mThreadList = new ArrayList<>(initialCapacity);
}
@Override
protected String[] doInBackground(String... urls) {
String[] result = new String[urls.length];
if (!NetworkUtils.isOnline(AppHeap.getContext())) return result;
for (String url : urls) {
if (TextUtils.isEmpty(url)) continue;
// Control the amount of running threads.
final int threadSize = mThreadList.size();
if (mReduceThreads ? threadSize > 1 : threadSize > MAX_THREAD_NUM) {
// Search for the best candidate to be
// finished.
LoaderThread thread = null;
for (int i = 0; i < threadSize; i++) {
thread = mThreadList.get(i);
if (!thread.isAlive()) {
// No need to search more,
// dead thread is a great choice.
break;
} else if (i == threadSize - 1) {
thread = mThreadList.get(0);
}
}
assert thread != null;
joinThread(thread);
mThreadList.remove(thread);
}
LoaderThread thread = new LoaderThread(mMap, url);
thread.start();
mThreadList.add(thread);
if (isCancelled()) {
fireThreads();
return null;
}
}
// Wait for all threads.
for (LoaderThread thread : mThreadList) {
joinThread(thread);
}
mThreadList.clear();
// Extract results to the array.
for (int i = 0; i < urls.length; i++) {
result[i] = mMap.get(urls[i]);
}
mMap.clear();
return result;
}
/**
* Joins given thread, if it is alive.
*
* @param thread thread to be joined
*/
private void joinThread(@NonNull LoaderThread thread) {
if (thread.isAlive()) {
while (true) {
try {
thread.join();
break;
} catch (InterruptedException e) { /* pretty please! */ }
// Well, at least it didn't explode.
}
}
}
private void fireThreads() { // is it correct?
for (LoaderThread thread : mThreadList) {
if (thread.isAlive() && !thread.isInterrupted()) {
thread.interrupt();
}
}
mThreadList.clear();
}
@Override
protected void onPostExecute(@NonNull String[] s) {
super.onPostExecute(s);
Callback callback = mCallback.get();
if (callback != null) {
callback.onDownloaded(s);
} else Timber.tag(TAG).d("Finished loading text, but callback is null!");
}
}
}