Implement Storage Access Framework
* re-work finished mission database * re-work DownloadMission and bump it Serializable version * keep the classic Java IO API * SAF Tree API support on Android Lollipop or higher * add wrapper for SAF stream opening * implement Closeable in SharpStream to replace the dispose() method * do required changes for this API: ** remove any file creation logic from DownloadInitializer ** make PostProcessing Serializable and reduce the number of iterations ** update all strings.xml files ** storage helpers: StoredDirectoryHelper & StoredFileHelper ** best effort to handle any kind of SAF errors/exceptions
This commit is contained in:
parent
8d8059229f
commit
7ca7952790
62 changed files with 2439 additions and 1180 deletions
|
|
@ -13,16 +13,15 @@ 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.service.DownloadManagerService.DMChecker;
|
||||
import us.shandian.giga.service.DownloadManagerService.MissionCheck;
|
||||
import us.shandian.giga.get.sqlite.FinishedMissionStore;
|
||||
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
import us.shandian.giga.util.Utility;
|
||||
|
||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
|
|
@ -36,7 +35,10 @@ public class DownloadManager {
|
|||
public final static int SPECIAL_PENDING = 1;
|
||||
public final static int SPECIAL_FINISHED = 2;
|
||||
|
||||
private final DownloadDataSource mDownloadDataSource;
|
||||
static final String TAG_AUDIO = "audio";
|
||||
static final String TAG_VIDEO = "video";
|
||||
|
||||
private final FinishedMissionStore mFinishedMissionStore;
|
||||
|
||||
private final ArrayList<DownloadMission> mMissionsPending = new ArrayList<>();
|
||||
private final ArrayList<FinishedMission> mMissionsFinished;
|
||||
|
|
@ -51,6 +53,9 @@ public class DownloadManager {
|
|||
boolean mPrefQueueLimit;
|
||||
private boolean mSelfMissionsControl;
|
||||
|
||||
StoredDirectoryHelper mMainStorageAudio;
|
||||
StoredDirectoryHelper mMainStorageVideo;
|
||||
|
||||
/**
|
||||
* Create a new instance
|
||||
*
|
||||
|
|
@ -62,7 +67,7 @@ public class DownloadManager {
|
|||
Log.d(TAG, "new DownloadManager instance. 0x" + Integer.toHexString(this.hashCode()));
|
||||
}
|
||||
|
||||
mDownloadDataSource = new DownloadDataSource(context);
|
||||
mFinishedMissionStore = new FinishedMissionStore(context);
|
||||
mHandler = handler;
|
||||
mMissionsFinished = loadFinishedMissions();
|
||||
mPendingMissionsDir = getPendingDir(context);
|
||||
|
|
@ -71,7 +76,7 @@ public class DownloadManager {
|
|||
throw new RuntimeException("failed to create pending_downloads in data directory");
|
||||
}
|
||||
|
||||
loadPendingMissions();
|
||||
loadPendingMissions(context);
|
||||
}
|
||||
|
||||
private static File getPendingDir(@NonNull Context context) {
|
||||
|
|
@ -92,29 +97,24 @@ public class DownloadManager {
|
|||
* Loads finished missions from the data source
|
||||
*/
|
||||
private ArrayList<FinishedMission> loadFinishedMissions() {
|
||||
ArrayList<FinishedMission> finishedMissions = mDownloadDataSource.loadFinishedMissions();
|
||||
ArrayList<FinishedMission> finishedMissions = mFinishedMissionStore.loadFinishedMissions();
|
||||
|
||||
// missions always is stored by creation order, simply reverse the list
|
||||
ArrayList<FinishedMission> result = new ArrayList<>(finishedMissions.size());
|
||||
// check if the files exists, otherwise, forget the download
|
||||
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;
|
||||
if (!mission.storage.existsAsFile()) {
|
||||
if (DEBUG) Log.d(TAG, "downloaded file removed: " + mission.storage.getName());
|
||||
|
||||
mFinishedMissionStore.deleteMission(mission);
|
||||
finishedMissions.remove(i);
|
||||
}
|
||||
|
||||
result.add(mission);
|
||||
}
|
||||
|
||||
return result;
|
||||
return finishedMissions;
|
||||
}
|
||||
|
||||
private void loadPendingMissions() {
|
||||
private void loadPendingMissions(Context ctx) {
|
||||
File[] subs = mPendingMissionsDir.listFiles();
|
||||
|
||||
if (subs == null) {
|
||||
|
|
@ -142,40 +142,63 @@ public class DownloadManager {
|
|||
continue;
|
||||
}
|
||||
|
||||
File dl = mis.getDownloadedFile();
|
||||
boolean exists = dl.exists();
|
||||
boolean exists;
|
||||
try {
|
||||
mis.storage = StoredFileHelper.deserialize(mis.storage, ctx);
|
||||
exists = !mis.storage.isInvalid() && mis.storage.existsAsFile();
|
||||
|
||||
} catch (Exception ex) {
|
||||
Log.e(TAG, "Failed to load the file source of " + mis.storage.toString());
|
||||
mis.storage.invalidate();
|
||||
exists = false;
|
||||
}
|
||||
|
||||
if (mis.isPsRunning()) {
|
||||
if (mis.postprocessingThis) {
|
||||
if (mis.psAlgorithm.worksOnSameFile) {
|
||||
// Incomplete post-processing results in a corrupted download file
|
||||
// because the selected algorithm works on the same file to save space.
|
||||
if (exists && dl.isFile() && !dl.delete())
|
||||
if (exists && !mis.storage.delete())
|
||||
Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath());
|
||||
|
||||
exists = true;
|
||||
}
|
||||
|
||||
mis.postprocessingState = 0;
|
||||
mis.psState = 0;
|
||||
mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED;
|
||||
mis.errObject = null;
|
||||
} else if (exists && !dl.isFile()) {
|
||||
// probably a folder, this should never happens
|
||||
if (!sub.delete()) {
|
||||
Log.w(TAG, "Unable to delete serialized file: " + sub.getPath());
|
||||
} else if (!exists) {
|
||||
|
||||
StoredDirectoryHelper mainStorage = getMainStorage(mis.storage.getTag());
|
||||
|
||||
if (!mis.storage.isInvalid() && !mis.storage.create()) {
|
||||
// using javaIO cannot recreate the file
|
||||
// using SAF in older devices (no tree available)
|
||||
//
|
||||
// force the user to pick again the save path
|
||||
mis.storage.invalidate();
|
||||
} else if (mainStorage != null) {
|
||||
// if the user has changed the save path before this download, the original save path will be lost
|
||||
StoredFileHelper newStorage = mainStorage.createFile(mis.storage.getName(), mis.storage.getType());
|
||||
if (newStorage == null)
|
||||
mis.storage.invalidate();
|
||||
else
|
||||
mis.storage = newStorage;
|
||||
}
|
||||
|
||||
if (mis.isInitialized()) {
|
||||
// the progress is lost, reset mission state
|
||||
DownloadMission m = new DownloadMission(mis.urls, mis.storage, mis.kind, mis.psAlgorithm);
|
||||
m.timestamp = mis.timestamp;
|
||||
m.threadCount = mis.threadCount;
|
||||
m.source = mis.source;
|
||||
m.nearLength = mis.nearLength;
|
||||
m.enqueued = mis.enqueued;
|
||||
m.errCode = DownloadMission.ERROR_PROGRESS_LOST;
|
||||
mis = m;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!exists && mis.isInitialized()) {
|
||||
// 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.nearLength = mis.nearLength;
|
||||
m.setEnqueued(mis.enqueued);
|
||||
mis = m;
|
||||
}
|
||||
if (mis.psAlgorithm != null) mis.psAlgorithm.cacheDir = ctx.getCacheDir();
|
||||
|
||||
mis.running = false;
|
||||
mis.recovered = exists;
|
||||
|
|
@ -196,51 +219,15 @@ public class DownloadManager {
|
|||
/**
|
||||
* 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 psName the name of the required post-processing algorithm, or {@code null} to ignore.
|
||||
* @param source source url of the resource
|
||||
* @param psArgs the arguments for the post-processing algorithm.
|
||||
* @param mission the new download mission to add and run (if possible)
|
||||
*/
|
||||
void startMission(String[] urls, String location, String name, char kind, int threads,
|
||||
String source, String psName, String[] psArgs, long nearLength) {
|
||||
void startMission(DownloadMission mission) {
|
||||
synchronized (this) {
|
||||
// check for existing pending download
|
||||
DownloadMission pendingMission = getPendingMission(location, name);
|
||||
|
||||
if (pendingMission != null) {
|
||||
if (pendingMission.running) {
|
||||
// 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 {
|
||||
// dispose the mission
|
||||
mMissionsPending.remove(pendingMission);
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED);
|
||||
pendingMission.delete();
|
||||
}
|
||||
} else {
|
||||
// check for existing finished download and dispose (if exists)
|
||||
int index = getFinishedMissionIndex(location, name);
|
||||
if (index >= 0) mDownloadDataSource.deleteMission(mMissionsFinished.remove(index));
|
||||
}
|
||||
|
||||
DownloadMission mission = new DownloadMission(urls, name, location, kind, psName, psArgs);
|
||||
mission.timestamp = System.currentTimeMillis();
|
||||
mission.threadCount = threads;
|
||||
mission.source = source;
|
||||
mission.mHandler = mHandler;
|
||||
mission.maxRetry = mPrefMaxRetry;
|
||||
mission.nearLength = nearLength;
|
||||
|
||||
// create metadata file
|
||||
while (true) {
|
||||
mission.metadata = new File(mPendingMissionsDir, String.valueOf(mission.timestamp));
|
||||
if (!mission.metadata.isFile() && !mission.metadata.exists()) {
|
||||
|
|
@ -261,6 +248,14 @@ public class DownloadManager {
|
|||
// Before continue, save the metadata in case the internet connection is not available
|
||||
Utility.writeToFile(mission.metadata, mission);
|
||||
|
||||
if (mission.storage == null) {
|
||||
// noting to do here
|
||||
mission.errCode = DownloadMission.ERROR_FILE_CREATION;
|
||||
if (mission.errObject != null)
|
||||
mission.errObject = new IOException("DownloadMission.storage == NULL");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean start = !mPrefQueueLimit || getRunningMissionsCount() < 1;
|
||||
|
||||
if (canDownloadInCurrentNetwork() && start) {
|
||||
|
|
@ -292,7 +287,7 @@ public class DownloadManager {
|
|||
mMissionsPending.remove(mission);
|
||||
} else if (mission instanceof FinishedMission) {
|
||||
mMissionsFinished.remove(mission);
|
||||
mDownloadDataSource.deleteMission(mission);
|
||||
mFinishedMissionStore.deleteMission(mission);
|
||||
}
|
||||
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED);
|
||||
|
|
@ -300,18 +295,35 @@ public class DownloadManager {
|
|||
}
|
||||
}
|
||||
|
||||
public void forgetMission(StoredFileHelper storage) {
|
||||
synchronized (this) {
|
||||
Mission mission = getAnyMission(storage);
|
||||
if (mission == null) return;
|
||||
|
||||
if (mission instanceof DownloadMission) {
|
||||
mMissionsPending.remove(mission);
|
||||
} else if (mission instanceof FinishedMission) {
|
||||
mMissionsFinished.remove(mission);
|
||||
mFinishedMissionStore.deleteMission(mission);
|
||||
}
|
||||
|
||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED);
|
||||
mission.storage = null;
|
||||
mission.delete();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a pending mission by its location and name
|
||||
* Get a pending mission by its path
|
||||
*
|
||||
* @param location the location
|
||||
* @param name the name
|
||||
* @param storage where the file possible is stored
|
||||
* @return the mission or null if no such mission exists
|
||||
*/
|
||||
@Nullable
|
||||
private DownloadMission getPendingMission(String location, String name) {
|
||||
private DownloadMission getPendingMission(StoredFileHelper storage) {
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (location.equalsIgnoreCase(mission.location) && name.equalsIgnoreCase(mission.name)) {
|
||||
if (mission.storage.equals(storage)) {
|
||||
return mission;
|
||||
}
|
||||
}
|
||||
|
|
@ -319,16 +331,14 @@ public class DownloadManager {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get a finished mission by its location and name
|
||||
* Get a finished mission by its path
|
||||
*
|
||||
* @param location the location
|
||||
* @param name the name
|
||||
* @param storage where the file possible is stored
|
||||
* @return the mission index or -1 if no such mission exists
|
||||
*/
|
||||
private int getFinishedMissionIndex(String location, String name) {
|
||||
private int getFinishedMissionIndex(StoredFileHelper storage) {
|
||||
for (int i = 0; i < mMissionsFinished.size(); i++) {
|
||||
FinishedMission mission = mMissionsFinished.get(i);
|
||||
if (location.equalsIgnoreCase(mission.location) && name.equalsIgnoreCase(mission.name)) {
|
||||
if (mMissionsFinished.get(i).storage.equals(storage)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
|
@ -336,12 +346,12 @@ public class DownloadManager {
|
|||
return -1;
|
||||
}
|
||||
|
||||
public Mission getAnyMission(String location, String name) {
|
||||
private Mission getAnyMission(StoredFileHelper storage) {
|
||||
synchronized (this) {
|
||||
Mission mission = getPendingMission(location, name);
|
||||
Mission mission = getPendingMission(storage);
|
||||
if (mission != null) return mission;
|
||||
|
||||
int idx = getFinishedMissionIndex(location, name);
|
||||
int idx = getFinishedMissionIndex(storage);
|
||||
if (idx >= 0) return mMissionsFinished.get(idx);
|
||||
}
|
||||
|
||||
|
|
@ -382,7 +392,7 @@ public class DownloadManager {
|
|||
|
||||
synchronized (this) {
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (mission.running || mission.isPsFailed() || mission.isFinished()) continue;
|
||||
if (mission.running || !mission.canDownload()) continue;
|
||||
|
||||
flag = true;
|
||||
mission.start();
|
||||
|
|
@ -392,58 +402,6 @@ public class DownloadManager {
|
|||
if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
|
|
@ -453,7 +411,7 @@ public class DownloadManager {
|
|||
synchronized (this) {
|
||||
mMissionsPending.remove(mission);
|
||||
mMissionsFinished.add(0, new FinishedMission(mission));
|
||||
mDownloadDataSource.addMission(mission);
|
||||
mFinishedMissionStore.addFinishedMission(mission);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -474,7 +432,8 @@ public class DownloadManager {
|
|||
|
||||
boolean flag = false;
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (mission.running || !mission.enqueued || mission.isFinished()) continue;
|
||||
if (mission.running || !mission.enqueued || mission.isFinished() || mission.hasInvalidStorage())
|
||||
continue;
|
||||
|
||||
resumeMission(mission);
|
||||
if (mPrefQueueLimit) return true;
|
||||
|
|
@ -496,7 +455,7 @@ public class DownloadManager {
|
|||
public void forgetFinishedDownloads() {
|
||||
synchronized (this) {
|
||||
for (FinishedMission mission : mMissionsFinished) {
|
||||
mDownloadDataSource.deleteMission(mission);
|
||||
mFinishedMissionStore.deleteMission(mission);
|
||||
}
|
||||
mMissionsFinished.clear();
|
||||
}
|
||||
|
|
@ -523,7 +482,7 @@ public class DownloadManager {
|
|||
int paused = 0;
|
||||
synchronized (this) {
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (mission.isFinished() || mission.isPsRunning()) continue;
|
||||
if (!mission.canDownload() || mission.isPsRunning()) continue;
|
||||
|
||||
if (mission.running && isMetered) {
|
||||
paused++;
|
||||
|
|
@ -565,24 +524,32 @@ public class DownloadManager {
|
|||
), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
void checkForRunningMission(String location, String name, DMChecker check) {
|
||||
MissionCheck result = MissionCheck.None;
|
||||
|
||||
public MissionState checkForExistingMission(StoredFileHelper storage) {
|
||||
synchronized (this) {
|
||||
DownloadMission pending = getPendingMission(location, name);
|
||||
DownloadMission pending = getPendingMission(storage);
|
||||
|
||||
if (pending == null) {
|
||||
if (getFinishedMissionIndex(location, name) >= 0) result = MissionCheck.Finished;
|
||||
if (getFinishedMissionIndex(storage) >= 0) return MissionState.Finished;
|
||||
} else {
|
||||
if (pending.isFinished()) {
|
||||
result = MissionCheck.Finished;// this never should happen (race-condition)
|
||||
return MissionState.Finished;// this never should happen (race-condition)
|
||||
} else {
|
||||
result = pending.running ? MissionCheck.PendingRunning : MissionCheck.Pending;
|
||||
return pending.running ? MissionState.PendingRunning : MissionState.Pending;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
check.callback(result);
|
||||
return MissionState.None;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private StoredDirectoryHelper getMainStorage(@NonNull String tag) {
|
||||
if (tag.equals(TAG_AUDIO)) return mMainStorageAudio;
|
||||
if (tag.equals(TAG_VIDEO)) return mMainStorageVideo;
|
||||
|
||||
Log.w(TAG, "Unknown download category, not [audio video]: " + String.valueOf(tag));
|
||||
|
||||
return null;// this never should happen
|
||||
}
|
||||
|
||||
public class MissionIterator extends DiffUtil.Callback {
|
||||
|
|
@ -689,7 +656,7 @@ public class DownloadManager {
|
|||
|
||||
synchronized (DownloadManager.this) {
|
||||
for (DownloadMission mission : mMissionsPending) {
|
||||
if (hidden.contains(mission) || mission.isPsFailed() || mission.isFinished())
|
||||
if (hidden.contains(mission) || mission.canDownload())
|
||||
continue;
|
||||
|
||||
if (mission.running)
|
||||
|
|
@ -720,7 +687,14 @@ public class DownloadManager {
|
|||
|
||||
@Override
|
||||
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
|
||||
return areItemsTheSame(oldItemPosition, newItemPosition);
|
||||
Object x = snapshot.get(oldItemPosition);
|
||||
Object y = current.get(newItemPosition);
|
||||
|
||||
if (x instanceof Mission && y instanceof Mission) {
|
||||
return ((Mission) x).storage.equals(((Mission) y).storage);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,11 +6,9 @@ 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.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
|
|
@ -21,12 +19,14 @@ import android.net.NetworkRequest;
|
|||
import android.net.Uri;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.NotificationCompat.Builder;
|
||||
import android.support.v4.content.PermissionChecker;
|
||||
|
|
@ -39,9 +39,13 @@ import org.schabi.newpipe.download.DownloadActivity;
|
|||
import org.schabi.newpipe.player.helper.LockManager;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
import us.shandian.giga.postprocessing.Postprocessing;
|
||||
import us.shandian.giga.service.DownloadManager.NetworkState;
|
||||
|
||||
import static org.schabi.newpipe.BuildConfig.APPLICATION_ID;
|
||||
|
|
@ -61,19 +65,19 @@ public class DownloadManagerService extends Service {
|
|||
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_PATH = "DownloadManagerService.extra.path";
|
||||
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 EXTRA_NEAR_LENGTH = "DownloadManagerService.extra.nearLength";
|
||||
private static final String EXTRA_MAIN_STORAGE_TAG = "DownloadManagerService.extra.tag";
|
||||
|
||||
private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished";
|
||||
private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished";
|
||||
|
||||
private DMBinder mBinder;
|
||||
private DownloadManagerBinder mBinder;
|
||||
private DownloadManager mManager;
|
||||
private Notification mNotification;
|
||||
private Handler mHandler;
|
||||
|
|
@ -110,10 +114,10 @@ public class DownloadManagerService extends Service {
|
|||
/**
|
||||
* notify media scanner on downloaded media file ...
|
||||
*
|
||||
* @param file the downloaded file
|
||||
* @param file the downloaded file uri
|
||||
*/
|
||||
private void notifyMediaScanner(File file) {
|
||||
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file)));
|
||||
private void notifyMediaScanner(Uri file) {
|
||||
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, file));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -124,7 +128,7 @@ public class DownloadManagerService extends Service {
|
|||
Log.d(TAG, "onCreate");
|
||||
}
|
||||
|
||||
mBinder = new DMBinder();
|
||||
mBinder = new DownloadManagerBinder();
|
||||
mHandler = new Handler(Looper.myLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
|
|
@ -186,10 +190,12 @@ public class DownloadManagerService extends Service {
|
|||
handlePreferenceChange(mPrefs, getString(R.string.downloads_queue_limit));
|
||||
|
||||
mLock = new LockManager(this);
|
||||
|
||||
setupStorageAPI(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
public int onStartCommand(final Intent intent, int flags, int startId) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, intent == null ? "Restarting" : "Starting");
|
||||
}
|
||||
|
|
@ -200,20 +206,7 @@ public class DownloadManagerService extends Service {
|
|||
String action = intent.getAction();
|
||||
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);
|
||||
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
|
||||
|
||||
handleConnectivityState(true);// first check the actual network status
|
||||
|
||||
mHandler.post(() -> mManager.startMission(urls, location, name, kind, threads, source, psName, psArgs, nearLength));
|
||||
|
||||
mHandler.post(() -> startMission(intent));
|
||||
} else if (downloadDoneNotification != null) {
|
||||
if (action.equals(ACTION_RESET_DOWNLOAD_FINISHED) || action.equals(ACTION_OPEN_DOWNLOADS_FINISHED)) {
|
||||
downloadDoneCount = 0;
|
||||
|
|
@ -264,12 +257,12 @@ public class DownloadManagerService extends Service {
|
|||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
int permissionCheck;
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
|
||||
permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
|
||||
if (permissionCheck == PermissionChecker.PERMISSION_DENIED) {
|
||||
Toast.makeText(this, "Permission denied (read)", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
|
||||
// permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
|
||||
// if (permissionCheck == PermissionChecker.PERMISSION_DENIED) {
|
||||
// Toast.makeText(this, "Permission denied (read)", Toast.LENGTH_SHORT).show();
|
||||
// }
|
||||
// }
|
||||
|
||||
permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||
if (permissionCheck == PermissionChecker.PERMISSION_DENIED) {
|
||||
|
|
@ -284,8 +277,8 @@ public class DownloadManagerService extends Service {
|
|||
|
||||
switch (msg.what) {
|
||||
case MESSAGE_FINISHED:
|
||||
notifyMediaScanner(mission.getDownloadedFile());
|
||||
notifyFinishedDownload(mission.name);
|
||||
notifyMediaScanner(mission.storage.getUri());
|
||||
notifyFinishedDownload(mission.storage.getName());
|
||||
mManager.setFinished(mission);
|
||||
handleConnectivityState(false);
|
||||
updateForegroundState(mManager.runMissions());
|
||||
|
|
@ -344,7 +337,7 @@ public class DownloadManagerService extends Service {
|
|||
if (key.equals(getString(R.string.downloads_maximum_retry))) {
|
||||
try {
|
||||
String value = prefs.getString(key, getString(R.string.downloads_maximum_retry_default));
|
||||
mManager.mPrefMaxRetry = Integer.parseInt(value);
|
||||
mManager.mPrefMaxRetry = value == null ? 0 : Integer.parseInt(value);
|
||||
} catch (Exception e) {
|
||||
mManager.mPrefMaxRetry = 0;
|
||||
}
|
||||
|
|
@ -353,6 +346,12 @@ public class DownloadManagerService extends Service {
|
|||
mManager.mPrefMeteredDownloads = prefs.getBoolean(key, false);
|
||||
} else if (key.equals(getString(R.string.downloads_queue_limit))) {
|
||||
mManager.mPrefQueueLimit = prefs.getBoolean(key, true);
|
||||
} else if (key.equals(getString(R.string.downloads_storage_api))) {
|
||||
setupStorageAPI(false);
|
||||
} else if (key.equals(getString(R.string.download_path_video_key))) {
|
||||
loadMainStorage(key, DownloadManager.TAG_VIDEO, false);
|
||||
} else if (key.equals(getString(R.string.download_path_audio_key))) {
|
||||
loadMainStorage(key, DownloadManager.TAG_AUDIO, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -370,43 +369,61 @@ public class DownloadManagerService extends Service {
|
|||
mForeground = state;
|
||||
}
|
||||
|
||||
public static void startMission(Context context, String urls[], String location, String name, char kind,
|
||||
/**
|
||||
* Start a new download mission
|
||||
*
|
||||
* @param context the activity context
|
||||
* @param urls the list of urls to download
|
||||
* @param storage where the file is saved
|
||||
* @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 psName the name of the required post-processing algorithm, or {@code null} to ignore.
|
||||
* @param source source url of the resource
|
||||
* @param psArgs the arguments for the post-processing algorithm.
|
||||
* @param nearLength the approximated final length of the file
|
||||
*/
|
||||
public static void startMission(Context context, String urls[], StoredFileHelper storage, char kind,
|
||||
int threads, String source, String psName, String[] psArgs, long nearLength) {
|
||||
Intent intent = new Intent(context, DownloadManagerService.class);
|
||||
intent.setAction(Intent.ACTION_RUN);
|
||||
intent.putExtra(EXTRA_URLS, urls);
|
||||
intent.putExtra(EXTRA_NAME, name);
|
||||
intent.putExtra(EXTRA_LOCATION, location);
|
||||
intent.putExtra(EXTRA_PATH, storage.getUri());
|
||||
intent.putExtra(EXTRA_KIND, kind);
|
||||
intent.putExtra(EXTRA_THREADS, threads);
|
||||
intent.putExtra(EXTRA_SOURCE, source);
|
||||
intent.putExtra(EXTRA_POSTPROCESSING_NAME, psName);
|
||||
intent.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs);
|
||||
intent.putExtra(EXTRA_NEAR_LENGTH, nearLength);
|
||||
intent.putExtra(EXTRA_MAIN_STORAGE_TAG, storage.getTag());
|
||||
context.startService(intent);
|
||||
}
|
||||
|
||||
public static void checkForRunningMission(Context context, String location, String name, DMChecker checker) {
|
||||
Intent intent = new Intent();
|
||||
intent.setClass(context, DownloadManagerService.class);
|
||||
context.startService(intent);
|
||||
public void startMission(Intent intent) {
|
||||
String[] urls = intent.getStringArrayExtra(EXTRA_URLS);
|
||||
Uri path = intent.getParcelableExtra(EXTRA_PATH);
|
||||
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);
|
||||
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
|
||||
String tag = intent.getStringExtra(EXTRA_MAIN_STORAGE_TAG);
|
||||
|
||||
context.bindService(intent, new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName cname, IBinder service) {
|
||||
try {
|
||||
((DMBinder) service).getDownloadManager().checkForRunningMission(location, name, checker);
|
||||
} catch (Exception err) {
|
||||
Log.w(TAG, "checkForRunningMission() callback is defective", err);
|
||||
}
|
||||
StoredFileHelper storage;
|
||||
try {
|
||||
storage = new StoredFileHelper(this, path, tag);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);// this never should happen
|
||||
}
|
||||
|
||||
context.unbindService(this);
|
||||
}
|
||||
final DownloadMission mission = new DownloadMission(urls, storage, kind, Postprocessing.getAlgorithm(psName, psArgs));
|
||||
mission.threadCount = threads;
|
||||
mission.source = source;
|
||||
mission.nearLength = nearLength;
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
}
|
||||
}, Context.BIND_AUTO_CREATE);
|
||||
handleConnectivityState(true);// first check the actual network status
|
||||
|
||||
mManager.startMission(mission);
|
||||
}
|
||||
|
||||
public void notifyFinishedDownload(String name) {
|
||||
|
|
@ -471,12 +488,12 @@ public class DownloadManagerService extends Service {
|
|||
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
downloadFailedNotification.setContentTitle(getString(R.string.app_name));
|
||||
downloadFailedNotification.setStyle(new NotificationCompat.BigTextStyle()
|
||||
.bigText(getString(R.string.download_failed).concat(": ").concat(mission.name)));
|
||||
.bigText(getString(R.string.download_failed).concat(": ").concat(mission.storage.getName())));
|
||||
} else {
|
||||
downloadFailedNotification.setContentTitle(getString(R.string.download_failed));
|
||||
downloadFailedNotification.setContentText(mission.name);
|
||||
downloadFailedNotification.setContentText(mission.storage.getName());
|
||||
downloadFailedNotification.setStyle(new NotificationCompat.BigTextStyle()
|
||||
.bigText(mission.name));
|
||||
.bigText(mission.storage.getName()));
|
||||
}
|
||||
|
||||
mNotificationManager.notify(id, downloadFailedNotification.build());
|
||||
|
|
@ -508,16 +525,81 @@ public class DownloadManagerService extends Service {
|
|||
mLockAcquired = acquire;
|
||||
}
|
||||
|
||||
private void setupStorageAPI(boolean acquire) {
|
||||
loadMainStorage(getString(R.string.download_path_audio_key), DownloadManager.TAG_VIDEO, acquire);
|
||||
loadMainStorage(getString(R.string.download_path_video_key), DownloadManager.TAG_AUDIO, acquire);
|
||||
}
|
||||
|
||||
void loadMainStorage(String prefKey, String tag, boolean acquire) {
|
||||
String path = mPrefs.getString(prefKey, null);
|
||||
|
||||
final String JAVA_IO = getString(R.string.downloads_storage_api_default);
|
||||
boolean useJavaIO = JAVA_IO.equals(mPrefs.getString(getString(R.string.downloads_storage_api), JAVA_IO));
|
||||
|
||||
final String defaultPath;
|
||||
if (tag.equals(DownloadManager.TAG_VIDEO))
|
||||
defaultPath = Environment.DIRECTORY_MOVIES;
|
||||
else// if (tag.equals(DownloadManager.TAG_AUDIO))
|
||||
defaultPath = Environment.DIRECTORY_MUSIC;
|
||||
|
||||
StoredDirectoryHelper mainStorage;
|
||||
if (path == null || path.isEmpty()) {
|
||||
mainStorage = useJavaIO ? new StoredDirectoryHelper(defaultPath, tag) : null;
|
||||
} else {
|
||||
|
||||
if (path.charAt(0) == File.separatorChar) {
|
||||
Log.i(TAG, "Migrating old save path: " + path);
|
||||
|
||||
useJavaIO = true;
|
||||
path = Uri.fromFile(new File(path)).toString();
|
||||
|
||||
mPrefs.edit().putString(prefKey, path).apply();
|
||||
}
|
||||
|
||||
if (useJavaIO) {
|
||||
mainStorage = new StoredDirectoryHelper(path, tag);
|
||||
} else {
|
||||
|
||||
// tree api is not available in older versions
|
||||
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
mainStorage = null;
|
||||
} else {
|
||||
try {
|
||||
mainStorage = new StoredDirectoryHelper(this, Uri.parse(path), tag);
|
||||
if (acquire) mainStorage.acquirePermissions();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to load the storage of " + tag + " from path: " + path, e);
|
||||
mainStorage = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tag.equals(DownloadManager.TAG_VIDEO))
|
||||
mManager.mMainStorageVideo = mainStorage;
|
||||
else// if (tag.equals(DownloadManager.TAG_AUDIO))
|
||||
mManager.mMainStorageAudio = mainStorage;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Wrappers for DownloadManager
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public class DMBinder extends Binder {
|
||||
public class DownloadManagerBinder extends Binder {
|
||||
public DownloadManager getDownloadManager() {
|
||||
return mManager;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public StoredDirectoryHelper getMainStorageVideo() {
|
||||
return mManager.mMainStorageVideo;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public StoredDirectoryHelper getMainStorageAudio() {
|
||||
return mManager.mMainStorageAudio;
|
||||
}
|
||||
|
||||
public void addMissionEventListener(Handler handler) {
|
||||
manageObservers(handler, true);
|
||||
}
|
||||
|
|
@ -548,10 +630,4 @@ public class DownloadManagerService extends Service {
|
|||
|
||||
}
|
||||
|
||||
public interface DMChecker {
|
||||
void callback(MissionCheck result);
|
||||
}
|
||||
|
||||
public enum MissionCheck {None, Pending, PendingRunning, Finished}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package us.shandian.giga.service;
|
||||
|
||||
public enum MissionState {
|
||||
None, Pending, PendingRunning, Finished
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue