main commit
Post-processing infrastructure * remove interfaces with one implementation * fix download resources with unknow length * marquee style for ProgressDrawable * "view details" option in mission context menu * notification for finished downloads * postprocessing infrastructure: sub-missions, circular file, layers for layers of abstractions for Java IO streams * Mp4 muxing (only DASH brand) * WebM muxing * Captions downloading * alert dialog for overwrite existing downloads finished or not. Misc changes * delete SQLiteDownloadDataSource.java * delete DownloadMissionSQLiteHelper.java * implement Localization from #114 Misc fixes (this branch) * restore old mission listeners variables. Prevents registered listeners get de-referenced on low-end devices * DownloadManagerService.checkForRunningMission() now return false if the mission has a error. * use Intent.FLAG_ACTIVITY_NEW_TASK when launching an activity from gigaget threads (apparently it is required in old versions of android) More changes * proper error handling "infrastructure" * queue instead of multiple downloads * move serialized pending downloads (.giga files) to app data * stop downloads when swicthing to mobile network (never works, see 2nd point) * save the thread count for next downloads * a lot of incoherences fixed * delete DownloadManagerTest.java (too many changes to keep this file updated)
This commit is contained in:
parent
2dc2a56671
commit
21e205a6be
48 changed files with 4379 additions and 1119 deletions
670
app/src/main/java/us/shandian/giga/service/DownloadManager.java
Normal file
670
app/src/main/java/us/shandian/giga/service/DownloadManager.java
Normal file
|
|
@ -0,0 +1,670 @@
|
|||
package us.shandian.giga.service;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Handler;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.util.DiffUtil;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.get.FinishedMission;
|
||||
import us.shandian.giga.get.Mission;
|
||||
import us.shandian.giga.get.sqlite.DownloadDataSource;
|
||||
import us.shandian.giga.util.Utility;
|
||||
|
||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
|
||||
public class DownloadManager {
|
||||
private static final String TAG = DownloadManager.class.getSimpleName();
|
||||
|
||||
enum NetworkState {Unavailable, WifiOperating, MobileOperating, OtherOperating}
|
||||
|
||||
public final static int SPECIAL_NOTHING = 0;
|
||||
public final static int SPECIAL_PENDING = 1;
|
||||
public final static int SPECIAL_FINISHED = 2;
|
||||
|
||||
private final DownloadDataSource mDownloadDataSource;
|
||||
|
||||
private final ArrayList<DownloadMission> mMissionsPending = new ArrayList<>();
|
||||
private final ArrayList<FinishedMission> mMissionsFinished;
|
||||
|
||||
private final Handler mHandler;
|
||||
private final File mPendingMissionsDir;
|
||||
|
||||
private NetworkState mLastNetworkStatus = NetworkState.Unavailable;
|
||||
|
||||
private SharedPreferences mPrefs;
|
||||
private String mPrefMaxRetry;
|
||||
private String mPrefCrossNetwork;
|
||||
|
||||
/**
|
||||
* Create a new instance
|
||||
*
|
||||
* @param context Context for the data source for finished downloads
|
||||
* @param handler Thread required for Messaging
|
||||
*/
|
||||
DownloadManager(@NonNull Context context, Handler handler) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "new DownloadManager instance. 0x" + Integer.toHexString(this.hashCode()));
|
||||
}
|
||||
|
||||
mDownloadDataSource = new DownloadDataSource(context);
|
||||
mHandler = handler;
|
||||
mMissionsFinished = loadFinishedMissions();
|
||||
mPendingMissionsDir = getPendingDir(context);
|
||||
mPrefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
mPrefMaxRetry = context.getString(R.string.downloads_max_retry);
|
||||
mPrefCrossNetwork = context.getString(R.string.cross_network_downloads);
|
||||
|
||||
if (!Utility.mkdir(mPendingMissionsDir, false)) {
|
||||
throw new RuntimeException("failed to create pending_downloads in data directory");
|
||||
}
|
||||
|
||||
loadPendingMissions();
|
||||
}
|
||||
|
||||
private static File getPendingDir(@NonNull Context context) {
|
||||
//File dir = new File(ContextCompat.getDataDir(context), "pending_downloads");
|
||||
File dir = context.getExternalFilesDir("pending_downloads");
|
||||
|
||||
if (dir == null) {
|
||||
// One of the following paths are not accessible ¿unmounted internal memory?
|
||||
// /storage/emulated/0/Android/data/org.schabi.newpipe[.debug]/pending_downloads
|
||||
// /sdcard/Android/data/org.schabi.newpipe[.debug]/pending_downloads
|
||||
Log.w(TAG, "path to pending downloads are not accessible");
|
||||
}
|
||||
|
||||
return dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads finished missions from the data source
|
||||
*/
|
||||
private ArrayList<FinishedMission> loadFinishedMissions() {
|
||||
ArrayList<FinishedMission> finishedMissions = mDownloadDataSource.loadFinishedMissions();
|
||||
|
||||
// missions always is stored by creation order, simply reverse the list
|
||||
ArrayList<FinishedMission> result = new ArrayList<>(finishedMissions.size());
|
||||
for (int i = finishedMissions.size() - 1; i >= 0; i--) {
|
||||
FinishedMission mission = finishedMissions.get(i);
|
||||
File file = mission.getDownloadedFile();
|
||||
|
||||
if (!file.isFile()) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "downloaded file removed: " + file.getAbsolutePath());
|
||||
}
|
||||
mDownloadDataSource.deleteMission(mission);
|
||||
continue;
|
||||
}
|
||||
|
||||
result.add(mission);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
private void loadPendingMissions() {
|
||||
File[] subs = mPendingMissionsDir.listFiles();
|
||||
|
||||
if (subs == null) {
|
||||
Log.e(TAG, "listFiles() returned null");
|
||||
return;
|
||||
}
|
||||
if (subs.length < 1) {
|
||||
return;
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Loading pending downloads from directory: " + mPendingMissionsDir.getAbsolutePath());
|
||||
}
|
||||
|
||||
for (File sub : subs) {
|
||||
if (sub.isFile()) {
|
||||
DownloadMission mis = Utility.readFromFile(sub);
|
||||
|
||||
if (mis == null) {
|
||||
sub.delete();
|
||||
} else {
|
||||
if (mis.isFinished()) {
|
||||
sub.delete();
|
||||
continue;
|
||||
}
|
||||
|
||||
File dl = mis.getDownloadedFile();
|
||||
boolean exists = dl.exists();
|
||||
|
||||
if (mis.postprocessingRunning && mis.postprocessingThis) {
|
||||
// Incomplete post-processing results in a corrupted download file
|
||||
// because the selected algorithm works on the same file to save space.
|
||||
if (!dl.delete()) {
|
||||
Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath());
|
||||
}
|
||||
exists = true;
|
||||
mis.postprocessingRunning = false;
|
||||
mis.errCode = DownloadMission.ERROR_POSTPROCESSING_FAILED;
|
||||
mis.errObject = new RuntimeException("post-processing stopped unexpectedly");
|
||||
}
|
||||
|
||||
if (exists && !dl.isFile()) {
|
||||
// probably a folder, this should never happens
|
||||
if (!sub.delete()) {
|
||||
Log.w(TAG, "Unable to delete serialized file: " + sub.getPath());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!exists) {
|
||||
// downloaded file deleted, reset mission state
|
||||
DownloadMission m = new DownloadMission(mis.urls, mis.name, mis.location, mis.kind, mis.postprocessingName, mis.postprocessingArgs);
|
||||
m.timestamp = mis.timestamp;
|
||||
m.threadCount = mis.threadCount;
|
||||
m.source = mis.source;
|
||||
m.maxRetry = mis.maxRetry;
|
||||
mis = m;
|
||||
}
|
||||
|
||||
mis.running = false;
|
||||
mis.recovered = exists;
|
||||
mis.metadata = sub;
|
||||
mis.mHandler = mHandler;
|
||||
|
||||
mMissionsPending.add(mis);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mMissionsPending.size() > 1) {
|
||||
Collections.sort(mMissionsPending, (mission1, mission2) -> Long.compare(mission1.timestamp, mission2.timestamp));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new download mission
|
||||
*
|
||||
* @param urls the list of urls to download
|
||||
* @param location the location
|
||||
* @param name the name of the file to create
|
||||
* @param kind type of file (a: audio v: video s: subtitle ?: file-extension defined)
|
||||
* @param threads the number of threads maximal used to download chunks of the file.
|
||||
* @param postprocessingName the name of the required post-processing algorithm, or {@code null} to ignore.
|
||||
* @param source source url of the resource
|
||||
* @param postProcessingArgs the arguments for the post-processing algorithm.
|
||||
*/
|
||||
void startMission(String[] urls, String location, String name, char kind, int threads, String source,
|
||||
String postprocessingName, String[] postProcessingArgs) {
|
||||
synchronized (this) {
|
||||
// check for existing pending download
|
||||
DownloadMission pendingMission = getPendingMission(location, name);
|
||||
if (pendingMission != null) {
|
||||
// generate unique filename (?)
|
||||
try {
|
||||
name = generateUniqueName(location, name);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Unable to generate unique name", e);
|
||||
name = System.currentTimeMillis() + name;
|
||||
Log.i(TAG, "Using " + name);
|
||||
}
|
||||
} else {
|
||||
// check for existing finished download
|
||||
int index = getFinishedMissionIndex(location, name);
|
||||
if (index >= 0) mDownloadDataSource.deleteMission(mMissionsFinished.remove(index));
|
||||
}
|
||||
|
||||
DownloadMission mission = new DownloadMission(urls, name, location, kind, postprocessingName, postProcessingArgs);
|
||||
mission.timestamp = System.currentTimeMillis();
|
||||
mission.threadCount = threads;
|
||||
mission.source = source;
|
||||
mission.mHandler = mHandler;
|
||||
mission.maxRetry = mPrefs.getInt(mPrefMaxRetry, 3);
|
||||
|
||||
while (true) {
|
||||
mission.metadata = new File(mPendingMissionsDir, String.valueOf(mission.timestamp));
|
||||
if (!mission.metadata.isFile() && !mission.metadata.exists()) {
|
||||
try {
|
||||
if (!mission.metadata.createNewFile())
|
||||
throw new RuntimeException("Cant create download metadata file");
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
mission.timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
mMissionsPending.add(mission);
|
||||
|
||||
// Before starting, save the state in case the internet connection is not available
|
||||
Utility.writeToFile(mission.metadata, mission);
|
||||
|
||||
if (canDownloadInCurrentNetwork() && (getRunningMissionsCount() < 1)) {
|
||||
mission.start();
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_RUNNING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void resumeMission(DownloadMission mission) {
|
||||
if (!mission.running) {
|
||||
mission.start();
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_RUNNING);
|
||||
}
|
||||
}
|
||||
|
||||
public void pauseMission(DownloadMission mission) {
|
||||
if (mission.running) {
|
||||
mission.pause();
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteMission(Mission mission) {
|
||||
synchronized (this) {
|
||||
if (mission instanceof DownloadMission) {
|
||||
mMissionsPending.remove(mission);
|
||||
} else if (mission instanceof FinishedMission) {
|
||||
mMissionsFinished.remove(mission);
|
||||
mDownloadDataSource.deleteMission(mission);
|
||||
}
|
||||
|
||||
mission.delete();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a pending mission by its location and name
|
||||
*
|
||||
* @param location the location
|
||||
* @param name the name
|
||||
* @return the mission or null if no such mission exists
|
||||
*/
|
||||
@Nullable
|
||||
private DownloadMission getPendingMission(String location, String name) {
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (location.equalsIgnoreCase(mission.location) && name.equalsIgnoreCase(mission.name)) {
|
||||
return mission;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a finished mission by its location and name
|
||||
*
|
||||
* @param location the location
|
||||
* @param name the name
|
||||
* @return the mission index or -1 if no such mission exists
|
||||
*/
|
||||
private int getFinishedMissionIndex(String location, String name) {
|
||||
for (int i = 0; i < mMissionsFinished.size(); i++) {
|
||||
FinishedMission mission = mMissionsFinished.get(i);
|
||||
if (location.equalsIgnoreCase(mission.location) && name.equalsIgnoreCase(mission.name)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public Mission getAnyMission(String location, String name) {
|
||||
synchronized (this) {
|
||||
Mission mission = getPendingMission(location, name);
|
||||
if (mission != null) return mission;
|
||||
|
||||
int idx = getFinishedMissionIndex(location, name);
|
||||
if (idx >= 0) return mMissionsFinished.get(idx);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
int getRunningMissionsCount() {
|
||||
int count = 0;
|
||||
synchronized (this) {
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (mission.running && mission.errCode != DownloadMission.ERROR_POSTPROCESSING_FAILED && !mission.isFinished())
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
void pauseAllMissions() {
|
||||
synchronized (this) {
|
||||
for (DownloadMission mission : mMissionsPending) mission.pause();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Splits the filename into name and extension
|
||||
* <p>
|
||||
* Dots are ignored if they appear: not at all, at the beginning of the file,
|
||||
* at the end of the file
|
||||
*
|
||||
* @param name the name to split
|
||||
* @return a string array with a length of 2 containing the name and the extension
|
||||
*/
|
||||
private static String[] splitName(String name) {
|
||||
int dotIndex = name.lastIndexOf('.');
|
||||
if (dotIndex <= 0 || (dotIndex == name.length() - 1)) {
|
||||
return new String[]{name, ""};
|
||||
} else {
|
||||
return new String[]{name.substring(0, dotIndex), name.substring(dotIndex + 1)};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique file name.
|
||||
* <p>
|
||||
* e.g. "myName (1).txt" if the name "myName.txt" exists.
|
||||
*
|
||||
* @param location the location (to check for existing files)
|
||||
* @param name the name of the file
|
||||
* @return the unique file name
|
||||
* @throws IllegalArgumentException if the location is not a directory
|
||||
* @throws SecurityException if the location is not readable
|
||||
*/
|
||||
private static String generateUniqueName(String location, String name) {
|
||||
if (location == null) throw new NullPointerException("location is null");
|
||||
if (name == null) throw new NullPointerException("name is null");
|
||||
File destination = new File(location);
|
||||
if (!destination.isDirectory()) {
|
||||
throw new IllegalArgumentException("location is not a directory: " + location);
|
||||
}
|
||||
final String[] nameParts = splitName(name);
|
||||
String[] existingName = destination.list((dir, name1) -> name1.startsWith(nameParts[0]));
|
||||
Arrays.sort(existingName);
|
||||
String newName;
|
||||
int downloadIndex = 0;
|
||||
do {
|
||||
newName = nameParts[0] + " (" + downloadIndex + ")." + nameParts[1];
|
||||
++downloadIndex;
|
||||
if (downloadIndex == 1000) { // Probably an error on our side
|
||||
throw new RuntimeException("Too many existing files");
|
||||
}
|
||||
} while (Arrays.binarySearch(existingName, newName) >= 0);
|
||||
return newName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a pending download as finished
|
||||
*
|
||||
* @param mission the desired mission
|
||||
* @return true if exits pending missions running, otherwise, false
|
||||
*/
|
||||
boolean setFinished(DownloadMission mission) {
|
||||
synchronized (this) {
|
||||
int i = mMissionsPending.indexOf(mission);
|
||||
mMissionsPending.remove(i);
|
||||
|
||||
mMissionsFinished.add(0, new FinishedMission(mission));
|
||||
mDownloadDataSource.addMission(mission);
|
||||
|
||||
if (mMissionsPending.size() < 1) return false;
|
||||
|
||||
i = getRunningMissionsCount();
|
||||
if (i > 0) return true;
|
||||
|
||||
// before returning, check the queue
|
||||
if (!canDownloadInCurrentNetwork()) return false;
|
||||
|
||||
for (DownloadMission mission1 : mMissionsPending) {
|
||||
if (!mission1.running && mission.errCode != DownloadMission.ERROR_POSTPROCESSING_FAILED && mission1.enqueued) {
|
||||
resumeMission(mMissionsPending.get(i));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public MissionIterator getIterator() {
|
||||
return new MissionIterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Forget all finished downloads, but, doesn't delete any file
|
||||
*/
|
||||
public void forgetFinishedDownloads() {
|
||||
synchronized (this) {
|
||||
for (FinishedMission mission : mMissionsFinished) {
|
||||
mDownloadDataSource.deleteMission(mission);
|
||||
}
|
||||
mMissionsFinished.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean canDownloadInCurrentNetwork() {
|
||||
if (mLastNetworkStatus == NetworkState.Unavailable) return false;
|
||||
return !(mPrefs.getBoolean(mPrefCrossNetwork, false) && mLastNetworkStatus == NetworkState.MobileOperating);
|
||||
}
|
||||
|
||||
void handleConnectivityChange(NetworkState currentStatus) {
|
||||
if (currentStatus == mLastNetworkStatus) return;
|
||||
|
||||
mLastNetworkStatus = currentStatus;
|
||||
boolean pauseOnMobile = mPrefs.getBoolean(mPrefCrossNetwork, false);
|
||||
|
||||
if (currentStatus == NetworkState.Unavailable) {
|
||||
return;
|
||||
} else if (currentStatus != NetworkState.MobileOperating || !pauseOnMobile) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean flag = false;
|
||||
synchronized (this) {
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (mission.running && mission.isFinished() && !mission.postprocessingRunning) {
|
||||
flag = true;
|
||||
mission.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast check for pending downloads. If exists, the user will be notified
|
||||
* TODO: call this method in somewhere
|
||||
*
|
||||
* @param context the application context
|
||||
*/
|
||||
public static void notifyUserPendingDownloads(Context context) {
|
||||
int pending = getPendingDir(context).list().length;
|
||||
if (pending < 1) return;
|
||||
|
||||
Toast.makeText(context, context.getString(
|
||||
R.string.msg_pending_downloads,
|
||||
String.valueOf(pending)
|
||||
), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
void checkForRunningMission(String location, String name, DownloadManagerService.DMChecker check) {
|
||||
boolean listed;
|
||||
boolean finished = false;
|
||||
|
||||
synchronized (this) {
|
||||
DownloadMission mission = getPendingMission(location, name);
|
||||
if (mission != null) {
|
||||
listed = true;
|
||||
} else {
|
||||
listed = getFinishedMissionIndex(location, name) >= 0;
|
||||
finished = listed;
|
||||
}
|
||||
}
|
||||
|
||||
check.callback(listed, finished);
|
||||
}
|
||||
|
||||
public class MissionIterator extends DiffUtil.Callback {
|
||||
final Object FINISHED = new Object();
|
||||
final Object PENDING = new Object();
|
||||
|
||||
ArrayList<Object> snapshot;
|
||||
ArrayList<Object> current;
|
||||
ArrayList<Mission> hidden;
|
||||
|
||||
private MissionIterator() {
|
||||
hidden = new ArrayList<>(2);
|
||||
current = null;
|
||||
snapshot = getSpecialItems();
|
||||
}
|
||||
|
||||
private ArrayList<Object> getSpecialItems() {
|
||||
synchronized (DownloadManager.this) {
|
||||
ArrayList<Mission> pending = new ArrayList<>(mMissionsPending);
|
||||
ArrayList<Mission> finished = new ArrayList<>(mMissionsFinished);
|
||||
ArrayList<Mission> remove = new ArrayList<>(hidden);
|
||||
|
||||
// hide missions (if required)
|
||||
Iterator<Mission> iterator = remove.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Mission mission = iterator.next();
|
||||
if (pending.remove(mission) || finished.remove(mission)) iterator.remove();
|
||||
}
|
||||
|
||||
int fakeTotal = pending.size();
|
||||
if (fakeTotal > 0) fakeTotal++;
|
||||
|
||||
fakeTotal += finished.size();
|
||||
if (finished.size() > 0) fakeTotal++;
|
||||
|
||||
ArrayList<Object> list = new ArrayList<>(fakeTotal);
|
||||
if (pending.size() > 0) {
|
||||
list.add(PENDING);
|
||||
list.addAll(pending);
|
||||
}
|
||||
if (finished.size() > 0) {
|
||||
list.add(FINISHED);
|
||||
list.addAll(finished);
|
||||
}
|
||||
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
public MissionItem getItem(int position) {
|
||||
Object object = snapshot.get(position);
|
||||
|
||||
if (object == PENDING) return new MissionItem(SPECIAL_PENDING);
|
||||
if (object == FINISHED) return new MissionItem(SPECIAL_FINISHED);
|
||||
|
||||
return new MissionItem(SPECIAL_NOTHING, (Mission) object);
|
||||
}
|
||||
|
||||
public int getSpecialAtItem(int position) {
|
||||
Object object = snapshot.get(position);
|
||||
|
||||
if (object == PENDING) return SPECIAL_PENDING;
|
||||
if (object == FINISHED) return SPECIAL_FINISHED;
|
||||
|
||||
return SPECIAL_NOTHING;
|
||||
}
|
||||
|
||||
public MissionItem getItemUnsafe(int position) {
|
||||
synchronized (DownloadManager.this) {
|
||||
int count = mMissionsPending.size();
|
||||
int count2 = mMissionsFinished.size();
|
||||
|
||||
if (count > 0) {
|
||||
position--;
|
||||
if (position == -1)
|
||||
return new MissionItem(SPECIAL_PENDING);
|
||||
else if (position < count)
|
||||
return new MissionItem(SPECIAL_NOTHING, mMissionsPending.get(position));
|
||||
else if (position == count && count2 > 0)
|
||||
return new MissionItem(SPECIAL_FINISHED);
|
||||
else
|
||||
position -= count;
|
||||
} else {
|
||||
if (count2 > 0 && position == 0) {
|
||||
return new MissionItem(SPECIAL_FINISHED);
|
||||
}
|
||||
}
|
||||
|
||||
position--;
|
||||
|
||||
if (count2 < 1) {
|
||||
throw new RuntimeException(
|
||||
String.format("Out of range. pending_count=%s finished_count=%s position=%s", count, count2, position)
|
||||
);
|
||||
}
|
||||
|
||||
return new MissionItem(SPECIAL_NOTHING, mMissionsFinished.get(position));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void start() {
|
||||
current = getSpecialItems();
|
||||
}
|
||||
|
||||
public void end() {
|
||||
snapshot = current;
|
||||
current = null;
|
||||
}
|
||||
|
||||
public void hide(Mission mission) {
|
||||
hidden.add(mission);
|
||||
}
|
||||
|
||||
public void unHide(Mission mission) {
|
||||
hidden.remove(mission);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int getOldListSize() {
|
||||
return snapshot.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNewListSize() {
|
||||
return current.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
|
||||
return snapshot.get(oldItemPosition) == current.get(newItemPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
|
||||
return areItemsTheSame(oldItemPosition, newItemPosition);
|
||||
}
|
||||
}
|
||||
|
||||
public class MissionItem {
|
||||
public int special;
|
||||
public Mission mission;
|
||||
|
||||
MissionItem(int s, Mission m) {
|
||||
special = s;
|
||||
mission = m;
|
||||
}
|
||||
|
||||
MissionItem(int s) {
|
||||
this(s, null);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -2,17 +2,25 @@ package us.shandian.giga.service;
|
|||
|
||||
import android.Manifest;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.ServiceConnection;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.support.v4.app.NotificationCompat.Builder;
|
||||
import android.support.v4.content.PermissionChecker;
|
||||
|
|
@ -21,48 +29,61 @@ import android.widget.Toast;
|
|||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.download.DownloadActivity;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
|
||||
import us.shandian.giga.get.DownloadDataSource;
|
||||
import us.shandian.giga.get.DownloadManager;
|
||||
import us.shandian.giga.get.DownloadManagerImpl;
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.get.sqlite.SQLiteDownloadDataSource;
|
||||
import us.shandian.giga.service.DownloadManager.NetworkState;
|
||||
|
||||
import static org.schabi.newpipe.BuildConfig.APPLICATION_ID;
|
||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
|
||||
public class DownloadManagerService extends Service {
|
||||
|
||||
private static final String TAG = DownloadManagerService.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* Message code of update messages stored as {@link Message#what}.
|
||||
*/
|
||||
private static final int UPDATE_MESSAGE = 0;
|
||||
private static final int NOTIFICATION_ID = 1000;
|
||||
public static final int MESSAGE_RUNNING = 1;
|
||||
public static final int MESSAGE_PAUSED = 2;
|
||||
public static final int MESSAGE_FINISHED = 3;
|
||||
public static final int MESSAGE_PROGRESS = 4;
|
||||
public static final int MESSAGE_ERROR = 5;
|
||||
|
||||
private static final int FOREGROUND_NOTIFICATION_ID = 1000;
|
||||
private static final int DOWNLOADS_NOTIFICATION_ID = 1001;
|
||||
|
||||
private static final String EXTRA_URLS = "DownloadManagerService.extra.urls";
|
||||
private static final String EXTRA_NAME = "DownloadManagerService.extra.name";
|
||||
private static final String EXTRA_LOCATION = "DownloadManagerService.extra.location";
|
||||
private static final String EXTRA_IS_AUDIO = "DownloadManagerService.extra.is_audio";
|
||||
private static final String EXTRA_KIND = "DownloadManagerService.extra.kind";
|
||||
private static final String EXTRA_THREADS = "DownloadManagerService.extra.threads";
|
||||
private static final String EXTRA_POSTPROCESSING_NAME = "DownloadManagerService.extra.postprocessingName";
|
||||
private static final String EXTRA_POSTPROCESSING_ARGS = "DownloadManagerService.extra.postprocessingArgs";
|
||||
private static final String EXTRA_SOURCE = "DownloadManagerService.extra.source";
|
||||
|
||||
private static final String ACTION_RESET_DOWNLOAD_COUNT = APPLICATION_ID + ".reset_download_count";
|
||||
|
||||
private DMBinder mBinder;
|
||||
private DownloadManager mManager;
|
||||
private Notification mNotification;
|
||||
private Handler mHandler;
|
||||
private long mLastTimeStamp = System.currentTimeMillis();
|
||||
private DownloadDataSource mDataSource;
|
||||
private int downloadDoneCount = 0;
|
||||
private Builder downloadDoneNotification = null;
|
||||
private StringBuilder downloadDoneList = null;
|
||||
NotificationManager notificationManager = null;
|
||||
private boolean mForeground = false;
|
||||
private final ArrayList<Handler> mEchoObservers = new ArrayList<>(1);
|
||||
|
||||
private BroadcastReceiver mNetworkStateListener;
|
||||
|
||||
private final MissionListener missionListener = new MissionListener();
|
||||
|
||||
|
||||
private void notifyMediaScanner(DownloadMission mission) {
|
||||
Uri uri = Uri.parse("file://" + mission.location + "/" + mission.name);
|
||||
// notify media scanner on downloaded media file ...
|
||||
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));
|
||||
/**
|
||||
* notify media scanner on downloaded media file ...
|
||||
*
|
||||
* @param file the downloaded file
|
||||
*/
|
||||
private void notifyMediaScanner(File file) {
|
||||
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -74,19 +95,14 @@ public class DownloadManagerService extends Service {
|
|||
}
|
||||
|
||||
mBinder = new DMBinder();
|
||||
if (mDataSource == null) {
|
||||
mDataSource = new SQLiteDownloadDataSource(this);
|
||||
}
|
||||
if (mManager == null) {
|
||||
ArrayList<String> paths = new ArrayList<>(2);
|
||||
paths.add(NewPipeSettings.getVideoDownloadPath(this));
|
||||
paths.add(NewPipeSettings.getAudioDownloadPath(this));
|
||||
mManager = new DownloadManagerImpl(paths, mDataSource, this);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "mManager == null");
|
||||
Log.d(TAG, "Download directory: " + paths);
|
||||
mHandler = new Handler(Looper.myLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
DownloadManagerService.this.handleMessage(msg);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mManager = new DownloadManager(this, mHandler);
|
||||
|
||||
Intent openDownloadListIntent = new Intent(this, DownloadActivity.class)
|
||||
.setAction(Intent.ACTION_MAIN);
|
||||
|
|
@ -105,56 +121,49 @@ public class DownloadManagerService extends Service {
|
|||
.setContentText(getString(R.string.msg_running_detail));
|
||||
|
||||
mNotification = builder.build();
|
||||
notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
HandlerThread thread = new HandlerThread("ServiceMessenger");
|
||||
thread.start();
|
||||
|
||||
mHandler = new Handler(thread.getLooper()) {
|
||||
mNetworkStateListener = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case UPDATE_MESSAGE: {
|
||||
int runningCount = 0;
|
||||
|
||||
for (int i = 0; i < mManager.getCount(); i++) {
|
||||
if (mManager.getMission(i).running) {
|
||||
runningCount++;
|
||||
}
|
||||
}
|
||||
updateState(runningCount);
|
||||
break;
|
||||
}
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false)) {
|
||||
handleConnectivityChange(null);
|
||||
return;
|
||||
}
|
||||
handleConnectivityChange(intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO));
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
private void startMissionAsync(final String url, final String location, final String name,
|
||||
final boolean isAudio, final int threads) {
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
int missionId = mManager.startMission(url, location, name, isAudio, threads);
|
||||
mBinder.onMissionAdded(mManager.getMission(missionId));
|
||||
}
|
||||
});
|
||||
registerReceiver(mNetworkStateListener, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (DEBUG) {
|
||||
if (intent == null) {
|
||||
Log.d(TAG, "Restarting");
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
Log.d(TAG, "Starting");
|
||||
}
|
||||
Log.i(TAG, "Got intent: " + intent);
|
||||
String action = intent.getAction();
|
||||
if (action != null && action.equals(Intent.ACTION_RUN)) {
|
||||
String name = intent.getStringExtra(EXTRA_NAME);
|
||||
String location = intent.getStringExtra(EXTRA_LOCATION);
|
||||
int threads = intent.getIntExtra(EXTRA_THREADS, 1);
|
||||
boolean isAudio = intent.getBooleanExtra(EXTRA_IS_AUDIO, false);
|
||||
String url = intent.getDataString();
|
||||
startMissionAsync(url, location, name, isAudio, threads);
|
||||
if (action != null) {
|
||||
if (action.equals(Intent.ACTION_RUN)) {
|
||||
String[] urls = intent.getStringArrayExtra(EXTRA_URLS);
|
||||
String name = intent.getStringExtra(EXTRA_NAME);
|
||||
String location = intent.getStringExtra(EXTRA_LOCATION);
|
||||
int threads = intent.getIntExtra(EXTRA_THREADS, 1);
|
||||
char kind = intent.getCharExtra(EXTRA_KIND, '?');
|
||||
String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME);
|
||||
String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS);
|
||||
String source = intent.getStringExtra(EXTRA_SOURCE);
|
||||
|
||||
mHandler.post(() -> mManager.startMission(urls, location, name, kind, threads, source, psName, psArgs));
|
||||
|
||||
} else if (downloadDoneNotification != null && action.equals(ACTION_RESET_DOWNLOAD_COUNT)) {
|
||||
downloadDoneCount = 0;
|
||||
downloadDoneList.setLength(0);
|
||||
}
|
||||
}
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
|
@ -167,11 +176,17 @@ public class DownloadManagerService extends Service {
|
|||
Log.d(TAG, "Destroying");
|
||||
}
|
||||
|
||||
for (int i = 0; i < mManager.getCount(); i++) {
|
||||
mManager.pauseMission(i);
|
||||
stopForeground(true);
|
||||
|
||||
if (notificationManager != null && downloadDoneNotification != null) {
|
||||
downloadDoneNotification.setDeleteIntent(null);// prevent NewPipe running when is killed, cleared from recent, etc
|
||||
notificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build());
|
||||
}
|
||||
|
||||
stopForeground(true);
|
||||
unregisterReceiver(mNetworkStateListener);
|
||||
|
||||
mManager.pauseAllMissions();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -192,53 +207,171 @@ public class DownloadManagerService extends Service {
|
|||
return mBinder;
|
||||
}
|
||||
|
||||
private void postUpdateMessage() {
|
||||
mHandler.sendEmptyMessage(UPDATE_MESSAGE);
|
||||
}
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MESSAGE_FINISHED:
|
||||
DownloadMission mission = (DownloadMission) msg.obj;
|
||||
notifyMediaScanner(mission.getDownloadedFile());
|
||||
notifyFinishedDownload(mission.name);
|
||||
updateForegroundState(mManager.setFinished(mission));
|
||||
break;
|
||||
case MESSAGE_RUNNING:
|
||||
case MESSAGE_PROGRESS:
|
||||
updateForegroundState(true);
|
||||
break;
|
||||
case MESSAGE_PAUSED:
|
||||
case MESSAGE_ERROR:
|
||||
updateForegroundState(mManager.getRunningMissionsCount() > 0);
|
||||
break;
|
||||
}
|
||||
|
||||
private void updateState(int runningCount) {
|
||||
if (runningCount == 0) {
|
||||
stopForeground(true);
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, mNotification);
|
||||
|
||||
synchronized (mEchoObservers) {
|
||||
Iterator<Handler> iterator = mEchoObservers.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Handler handler = iterator.next();
|
||||
if (handler.getLooper().getThread().isAlive()) {
|
||||
Message echo = new Message();
|
||||
echo.what = msg.what;
|
||||
echo.obj = msg.obj;
|
||||
handler.sendMessage(echo);
|
||||
} else {
|
||||
iterator.remove();// ¿missing call to removeMissionEventListener()?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void startMission(Context context, String url, String location, String name, boolean isAudio, int threads) {
|
||||
private void handleConnectivityChange(NetworkInfo info) {
|
||||
NetworkState status;
|
||||
|
||||
if (info == null) {
|
||||
status = NetworkState.Unavailable;
|
||||
Log.i(TAG, "actual connectivity status is unavailable");
|
||||
} else if (!info.isAvailable() || !info.isConnected()) {
|
||||
status = NetworkState.Unavailable;
|
||||
Log.i(TAG, "actual connectivity status is not available and not connected");
|
||||
} else {
|
||||
int type = info.getType();
|
||||
if (type == ConnectivityManager.TYPE_MOBILE || type == ConnectivityManager.TYPE_MOBILE_DUN) {
|
||||
status = NetworkState.MobileOperating;
|
||||
} else if (type == ConnectivityManager.TYPE_WIFI) {
|
||||
status = NetworkState.WifiOperating;
|
||||
} else if (type == ConnectivityManager.TYPE_WIMAX ||
|
||||
type == ConnectivityManager.TYPE_ETHERNET ||
|
||||
type == ConnectivityManager.TYPE_BLUETOOTH) {
|
||||
status = NetworkState.OtherOperating;
|
||||
} else {
|
||||
status = NetworkState.Unavailable;
|
||||
}
|
||||
Log.i(TAG, "actual connectivity status is " + status.name());
|
||||
}
|
||||
|
||||
if (mManager == null) return;// avoid race-conditions while the service is starting
|
||||
mManager.handleConnectivityChange(status);
|
||||
}
|
||||
|
||||
public void updateForegroundState(boolean state) {
|
||||
if (state == mForeground) return;
|
||||
|
||||
if (state) {
|
||||
startForeground(FOREGROUND_NOTIFICATION_ID, mNotification);
|
||||
} else {
|
||||
stopForeground(true);
|
||||
}
|
||||
|
||||
mForeground = state;
|
||||
}
|
||||
|
||||
public static void startMission(Context context, String urls[], String location, String name,
|
||||
char kind, int threads, String source, String postprocessingName,
|
||||
String[] postprocessingArgs) {
|
||||
Intent intent = new Intent(context, DownloadManagerService.class);
|
||||
intent.setAction(Intent.ACTION_RUN);
|
||||
intent.setData(Uri.parse(url));
|
||||
intent.putExtra(EXTRA_URLS, urls);
|
||||
intent.putExtra(EXTRA_NAME, name);
|
||||
intent.putExtra(EXTRA_LOCATION, location);
|
||||
intent.putExtra(EXTRA_IS_AUDIO, isAudio);
|
||||
intent.putExtra(EXTRA_KIND, kind);
|
||||
intent.putExtra(EXTRA_THREADS, threads);
|
||||
intent.putExtra(EXTRA_SOURCE, source);
|
||||
intent.putExtra(EXTRA_POSTPROCESSING_NAME, postprocessingName);
|
||||
intent.putExtra(EXTRA_POSTPROCESSING_ARGS, postprocessingArgs);
|
||||
context.startService(intent);
|
||||
}
|
||||
|
||||
public static void checkForRunningMission(Context context, String location, String name, DMChecker check) {
|
||||
Intent intent = new Intent();
|
||||
intent.setClass(context, DownloadManagerService.class);
|
||||
context.bindService(intent, new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName cname, IBinder service) {
|
||||
try {
|
||||
((DMBinder) service).getDownloadManager().checkForRunningMission(location, name, check);
|
||||
} catch (Exception err) {
|
||||
Log.w(TAG, "checkForRunningMission() callback is defective", err);
|
||||
}
|
||||
|
||||
private class MissionListener implements DownloadMission.MissionListener {
|
||||
@Override
|
||||
public void onProgressUpdate(DownloadMission downloadMission, long done, long total) {
|
||||
long now = System.currentTimeMillis();
|
||||
long delta = now - mLastTimeStamp;
|
||||
if (delta > 2000) {
|
||||
postUpdateMessage();
|
||||
mLastTimeStamp = now;
|
||||
// TODO: find a efficient way to unbind the service. This destroy the service due idle, but is started again when the user start a download.
|
||||
context.unbindService(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinish(DownloadMission downloadMission) {
|
||||
postUpdateMessage();
|
||||
notifyMediaScanner(downloadMission);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(DownloadMission downloadMission, int errCode) {
|
||||
postUpdateMessage();
|
||||
}
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
}
|
||||
}, Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
public void notifyFinishedDownload(String name) {
|
||||
if (notificationManager == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (downloadDoneNotification == null) {
|
||||
downloadDoneList = new StringBuilder(name.length());
|
||||
|
||||
Bitmap icon = BitmapFactory.decodeResource(this.getResources(), android.R.drawable.stat_sys_download_done);
|
||||
downloadDoneNotification = new Builder(this, getString(R.string.notification_channel_id))
|
||||
.setAutoCancel(true)
|
||||
.setLargeIcon(icon)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setDeleteIntent(PendingIntent.getService(this, (int) System.currentTimeMillis(),
|
||||
new Intent(this, DownloadManagerService.class)
|
||||
.setAction(ACTION_RESET_DOWNLOAD_COUNT)
|
||||
, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setContentIntent(mNotification.contentIntent);
|
||||
}
|
||||
|
||||
if (downloadDoneCount < 1) {
|
||||
downloadDoneList.append(name);
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
downloadDoneNotification.setContentTitle(getString(R.string.app_name));
|
||||
downloadDoneNotification.setContentText(getString(R.string.download_finished, name));
|
||||
} else {
|
||||
downloadDoneNotification.setContentTitle(getString(R.string.download_finished, name));
|
||||
downloadDoneNotification.setContentText(null);
|
||||
}
|
||||
} else {
|
||||
downloadDoneList.append(", ");
|
||||
downloadDoneList.append(name);
|
||||
|
||||
downloadDoneNotification.setContentTitle(getString(R.string.download_finished_more, String.valueOf(downloadDoneCount + 1)));
|
||||
downloadDoneNotification.setContentText(downloadDoneList.toString());
|
||||
}
|
||||
|
||||
notificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build());
|
||||
downloadDoneCount++;
|
||||
}
|
||||
|
||||
private void manageObservers(Handler handler, boolean add) {
|
||||
synchronized (mEchoObservers) {
|
||||
if (add) {
|
||||
mEchoObservers.add(handler);
|
||||
} else {
|
||||
mEchoObservers.remove(handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper of DownloadManager
|
||||
public class DMBinder extends Binder {
|
||||
|
|
@ -246,14 +379,24 @@ public class DownloadManagerService extends Service {
|
|||
return mManager;
|
||||
}
|
||||
|
||||
public void onMissionAdded(DownloadMission mission) {
|
||||
mission.addListener(missionListener);
|
||||
postUpdateMessage();
|
||||
public void addMissionEventListener(Handler handler) {
|
||||
manageObservers(handler, true);
|
||||
}
|
||||
|
||||
public void onMissionRemoved(DownloadMission mission) {
|
||||
mission.removeListener(missionListener);
|
||||
postUpdateMessage();
|
||||
public void removeMissionEventListener(Handler handler) {
|
||||
manageObservers(handler, false);
|
||||
}
|
||||
|
||||
public void resetFinishedDownloadCount() {
|
||||
if (notificationManager == null || downloadDoneNotification == null) return;
|
||||
notificationManager.cancel(DOWNLOADS_NOTIFICATION_ID);
|
||||
downloadDoneList.setLength(0);
|
||||
downloadDoneCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public interface DMChecker {
|
||||
void callback(boolean listed, boolean finished);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue