package us.shandian.giga.service; import android.content.Context; import android.os.Handler; 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.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.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; public class DownloadManager { private static final String TAG = DownloadManager.class.getSimpleName(); enum NetworkState {Unavailable, Operating, MeteredOperating} public final static int SPECIAL_NOTHING = 0; public final static int SPECIAL_PENDING = 1; public final static int SPECIAL_FINISHED = 2; static final String TAG_AUDIO = "audio"; static final String TAG_VIDEO = "video"; private final FinishedMissionStore mFinishedMissionStore; private final ArrayList mMissionsPending = new ArrayList<>(); private final ArrayList mMissionsFinished; private final Handler mHandler; private final File mPendingMissionsDir; private NetworkState mLastNetworkStatus = NetworkState.Unavailable; int mPrefMaxRetry; boolean mPrefMeteredDownloads; boolean mPrefQueueLimit; private boolean mSelfMissionsControl; StoredDirectoryHelper mMainStorageAudio; StoredDirectoryHelper mMainStorageVideo; /** * 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())); } mFinishedMissionStore = new FinishedMissionStore(context); mHandler = handler; mMissionsFinished = loadFinishedMissions(); mPendingMissionsDir = getPendingDir(context); if (!Utility.mkdir(mPendingMissionsDir, false)) { throw new RuntimeException("failed to create pending_downloads in data directory"); } loadPendingMissions(context); } 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 loadFinishedMissions() { ArrayList finishedMissions = mFinishedMissionStore.loadFinishedMissions(); // check if the files exists, otherwise, forget the download for (int i = finishedMissions.size() - 1; i >= 0; i--) { FinishedMission mission = finishedMissions.get(i); if (!mission.storage.existsAsFile()) { if (DEBUG) Log.d(TAG, "downloaded file removed: " + mission.storage.getName()); mFinishedMissionStore.deleteMission(mission); finishedMissions.remove(i); } } return finishedMissions; } private void loadPendingMissions(Context ctx) { 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) { //noinspection ResultOfMethodCallIgnored sub.delete(); } else { if (mis.isFinished()) { //noinspection ResultOfMethodCallIgnored sub.delete(); continue; } 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.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 && !mis.storage.delete()) Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath()); exists = true; } mis.psState = 0; mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED; mis.errObject = null; } 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; } } if (mis.psAlgorithm != null) mis.psAlgorithm.cacheDir = ctx.getCacheDir(); mis.running = false; mis.recovered = exists; mis.metadata = sub; mis.maxRetry = mPrefMaxRetry; 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 mission the new download mission to add and run (if possible) */ void startMission(DownloadMission mission) { synchronized (this) { mission.timestamp = System.currentTimeMillis(); mission.mHandler = mHandler; mission.maxRetry = mPrefMaxRetry; // create metadata file 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(); } mSelfMissionsControl = true; mMissionsPending.add(mission); // 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) { mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS); mission.start(); } } } public void resumeMission(DownloadMission mission) { if (!mission.running) { mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS); mission.start(); } } public void pauseMission(DownloadMission mission) { if (mission.running) { mission.setEnqueued(false); 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); mFinishedMissionStore.deleteMission(mission); } mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED); mission.delete(); } } 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 path * * @param storage where the file possible is stored * @return the mission or null if no such mission exists */ @Nullable private DownloadMission getPendingMission(StoredFileHelper storage) { for (DownloadMission mission : mMissionsPending) { if (mission.storage.equals(storage)) { return mission; } } return null; } /** * Get a finished mission by its path * * @param storage where the file possible is stored * @return the mission index or -1 if no such mission exists */ private int getFinishedMissionIndex(StoredFileHelper storage) { for (int i = 0; i < mMissionsFinished.size(); i++) { if (mMissionsFinished.get(i).storage.equals(storage)) { return i; } } return -1; } private Mission getAnyMission(StoredFileHelper storage) { synchronized (this) { Mission mission = getPendingMission(storage); if (mission != null) return mission; int idx = getFinishedMissionIndex(storage); if (idx >= 0) return mMissionsFinished.get(idx); } return null; } int getRunningMissionsCount() { int count = 0; synchronized (this) { for (DownloadMission mission : mMissionsPending) { if (mission.running && !mission.isPsFailed() && !mission.isFinished()) count++; } } return count; } public void pauseAllMissions(boolean force) { boolean flag = false; synchronized (this) { for (DownloadMission mission : mMissionsPending) { if (!mission.running || mission.isPsRunning() || mission.isFinished()) continue; if (force) mission.threads = null;// avoid waiting for threads mission.pause(); flag = true; } } if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED); } public void startAllMissions() { boolean flag = false; synchronized (this) { for (DownloadMission mission : mMissionsPending) { if (mission.running || !mission.canDownload()) continue; flag = true; mission.start(); } } if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS); } /** * Set a pending download as finished * * @param mission the desired mission */ void setFinished(DownloadMission mission) { synchronized (this) { mMissionsPending.remove(mission); mMissionsFinished.add(0, new FinishedMission(mission)); mFinishedMissionStore.addFinishedMission(mission); } } /** * runs one or multiple missions in from queue if possible * * @return true if one or multiple missions are running, otherwise, false */ boolean runMissions() { synchronized (this) { if (mMissionsPending.size() < 1) return false; if (!canDownloadInCurrentNetwork()) return false; if (mPrefQueueLimit) { for (DownloadMission mission : mMissionsPending) if (!mission.isFinished() && mission.running) return true; } boolean flag = false; for (DownloadMission mission : mMissionsPending) { if (mission.running || !mission.enqueued || mission.isFinished() || mission.hasInvalidStorage()) continue; resumeMission(mission); if (mPrefQueueLimit) return true; flag = true; } return flag; } } public MissionIterator getIterator() { mSelfMissionsControl = true; return new MissionIterator(); } /** * Forget all finished downloads, but, doesn't delete any file */ public void forgetFinishedDownloads() { synchronized (this) { for (FinishedMission mission : mMissionsFinished) { mFinishedMissionStore.deleteMission(mission); } mMissionsFinished.clear(); } } private boolean canDownloadInCurrentNetwork() { if (mLastNetworkStatus == NetworkState.Unavailable) return false; return !(mPrefMeteredDownloads && mLastNetworkStatus == NetworkState.MeteredOperating); } void handleConnectivityState(NetworkState currentStatus, boolean updateOnly) { if (currentStatus == mLastNetworkStatus) return; mLastNetworkStatus = currentStatus; if (currentStatus == NetworkState.Unavailable) return; if (!mSelfMissionsControl || updateOnly) { return;// don't touch anything without the user interaction } boolean isMetered = mPrefMeteredDownloads && mLastNetworkStatus == NetworkState.MeteredOperating; int running = 0; int paused = 0; synchronized (this) { for (DownloadMission mission : mMissionsPending) { if (!mission.canDownload() || mission.isPsRunning()) continue; if (mission.running && isMetered) { paused++; mission.pause(); } else if (!mission.running && !isMetered && mission.enqueued) { running++; mission.start(); if (mPrefQueueLimit) break; } } } if (running > 0) { mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS); return; } if (paused > 0) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED); } void updateMaximumAttempts() { synchronized (this) { for (DownloadMission mission : mMissionsPending) mission.maxRetry = mPrefMaxRetry; } } /** * 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(); } public MissionState checkForExistingMission(StoredFileHelper storage) { synchronized (this) { DownloadMission pending = getPendingMission(storage); if (pending == null) { if (getFinishedMissionIndex(storage) >= 0) return MissionState.Finished; } else { if (pending.isFinished()) { return MissionState.Finished;// this never should happen (race-condition) } else { return pending.running ? MissionState.PendingRunning : MissionState.Pending; } } } 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 { final Object FINISHED = new Object(); final Object PENDING = new Object(); ArrayList snapshot; ArrayList current; ArrayList hidden; boolean hasFinished = false; private MissionIterator() { hidden = new ArrayList<>(2); current = null; snapshot = getSpecialItems(); } private ArrayList getSpecialItems() { synchronized (DownloadManager.this) { ArrayList pending = new ArrayList<>(mMissionsPending); ArrayList finished = new ArrayList<>(mMissionsFinished); ArrayList remove = new ArrayList<>(hidden); // hide missions (if required) Iterator 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 list = new ArrayList<>(fakeTotal); if (pending.size() > 0) { list.add(PENDING); list.addAll(pending); } if (finished.size() > 0) { list.add(FINISHED); list.addAll(finished); } hasFinished = finished.size() > 0; 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 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); } public boolean hasFinishedMissions() { return hasFinished; } /** * Check if exists missions running and paused. Corrupted and hidden missions are not counted * * @return two-dimensional array contains the current missions state. * 1° entry: true if has at least one mission running * 2° entry: true if has at least one mission paused */ public boolean[] hasValidPendingMissions() { boolean running = false; boolean paused = false; synchronized (DownloadManager.this) { for (DownloadMission mission : mMissionsPending) { if (hidden.contains(mission) || mission.canDownload()) continue; if (mission.running) paused = true; else running = true; } } return new boolean[]{running, paused}; } @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) { 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; } } public class MissionItem { public int special; public Mission mission; MissionItem(int s, Mission m) { special = s; mission = m; } MissionItem(int s) { this(s, null); } } }