diff --git a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java index 720e0f216..70e0d9fb1 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -2,6 +2,15 @@ package org.schabi.newpipe.fragments; import android.content.Context; import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.material.tabs.TabLayout; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; +import androidx.viewpager.widget.ViewPager; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; diff --git a/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java b/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java index e6363e423..37bf9c6d7 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java @@ -1,6 +1,6 @@ package org.schabi.newpipe.streams; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; import org.schabi.newpipe.streams.WebMReader.Cluster; import org.schabi.newpipe.streams.WebMReader.Segment; diff --git a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java index 17a2a7403..618200f27 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java @@ -1,9 +1,10 @@ package us.shandian.giga.get; -import androidx.annotation.NonNull; import android.text.TextUtils; import android.util.Log; +import androidx.annotation.NonNull; + import org.schabi.newpipe.streams.io.SharpStream; import java.io.IOException; @@ -177,7 +178,7 @@ public class DownloadInitializer extends Thread { if (e instanceof DownloadMission.HttpError && ((DownloadMission.HttpError) e).statusCode == ERROR_HTTP_FORBIDDEN) { // for youtube streams. The url has expired interrupt(); - mMission.doRecover(e); + mMission.doRecover(ERROR_HTTP_FORBIDDEN); return; } diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMission.java b/app/src/main/java/us/shandian/giga/get/DownloadMission.java index 918d6dbea..5ef72162c 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -4,18 +4,21 @@ import android.os.Handler; import android.util.Log; import androidx.annotation.Nullable; +import androidx.annotation.NonNull; import org.schabi.newpipe.DownloaderImpl; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InterruptedIOException; import java.io.Serializable; import java.net.ConnectException; import java.net.HttpURLConnection; import java.net.SocketTimeoutException; import java.net.URL; import java.net.UnknownHostException; +import java.nio.channels.ClosedByInterruptException; import javax.net.ssl.SSLException; @@ -27,7 +30,7 @@ import us.shandian.giga.util.Utility; import static org.schabi.newpipe.BuildConfig.DEBUG; public class DownloadMission extends Mission { - private static final long serialVersionUID = 6L;// last bump: 28 september 2019 + private static final long serialVersionUID = 6L;// last bump: 07 october 2019 static final int BUFFER_SIZE = 64 * 1024; static final int BLOCK_SIZE = 512 * 1024; @@ -61,9 +64,9 @@ public class DownloadMission extends Mission { public String[] urls; /** - * Number of bytes downloaded + * Number of bytes downloaded and written */ - public long done; + public volatile long done; /** * Indicates a file generated dynamically on the web server @@ -119,7 +122,7 @@ public class DownloadMission extends Mission { /** * Download/File resume offset in fallback mode (if applicable) {@link DownloadRunnableFallback} */ - long fallbackResumeOffset; + volatile long fallbackResumeOffset; /** * Maximum of download threads running, chosen by the user @@ -132,22 +135,23 @@ public class DownloadMission extends Mission { public MissionRecoveryInfo[] recoveryInfo; private transient int finishCount; - public transient boolean running; + public transient volatile boolean running; public boolean enqueued; public int errCode = ERROR_NOTHING; public Exception errObject = null; public transient Handler mHandler; - private transient boolean mWritingToFile; private transient boolean[] blockAcquired; + private transient long writingToFileNext; + private transient volatile boolean writingToFile; + final Object LOCK = new Lock(); - private transient boolean deleted; - - public transient volatile Thread[] threads = new Thread[0]; - private transient Thread init = null; + @NonNull + public transient Thread[] threads = new Thread[0]; + public transient Thread init = null; public DownloadMission(String[] urls, StoredFileHelper storage, char kind, Postprocessing psInstance) { if (urls == null) throw new NullPointerException("urls is null"); @@ -246,8 +250,10 @@ public class DownloadMission extends Mission { int statusCode = conn.getResponseCode(); if (DEBUG) { - Log.d(TAG, threadId + ":Range=" + conn.getRequestProperty("Range")); - Log.d(TAG, threadId + ":Content-Length=" + conn.getContentLength() + " Code:" + statusCode); + Log.d(TAG, threadId + ":[request] Range=" + conn.getRequestProperty("Range")); + Log.d(TAG, threadId + ":[response] Code=" + statusCode); + Log.d(TAG, threadId + ":[response] Content-Length=" + conn.getContentLength()); + Log.d(TAG, threadId + ":[response] Content-Range=" + conn.getHeaderField("Content-Range")); } @@ -272,24 +278,19 @@ public class DownloadMission extends Mission { } synchronized void notifyProgress(long deltaLen) { - if (!running) return; - if (unknownLength) { length += deltaLen;// Update length before proceeding } done += deltaLen; - if (done > length) { - done = length; - } + if (metadata == null) return; - if (done != length && !deleted && !mWritingToFile) { - mWritingToFile = true; - runAsync(-2, this::writeThisToFile); + if (!writingToFile && (done > writingToFileNext || deltaLen < 0)) { + writingToFile = true; + writingToFileNext = done + BLOCK_SIZE; + writeThisToFileAsync(); } - - notify(DownloadManagerService.MESSAGE_PROGRESS); } synchronized void notifyError(Exception err) { @@ -342,43 +343,42 @@ public class DownloadMission extends Mission { notify(DownloadManagerService.MESSAGE_ERROR); - if (running) { - running = false; - if (threads != null) selfPause(); - } + if (running) pauseThreads(); } synchronized void notifyFinished() { - if (errCode > ERROR_NOTHING) return; - - finishCount++; - - if (blocks.length < 1 || threads == null || finishCount == threads.length) { - if (errCode != ERROR_NOTHING) return; + if (current < urls.length) { + if (++finishCount < threads.length) return; if (DEBUG) { - Log.d(TAG, "onFinish: " + (current + 1) + "/" + urls.length); - } - - if ((current + 1) < urls.length) { - // prepare next sub-mission - long current_offset = offsets[current++]; - offsets[current] = current_offset + length; - initializer(); - return; + Log.d(TAG, "onFinish: downloaded " + (current + 1) + "/" + urls.length); } current++; - unknownLength = false; - - if (!doPostprocessing()) return; - - enqueued = false; - running = false; - deleteThisFromFile(); - - notify(DownloadManagerService.MESSAGE_FINISHED); + if (current < urls.length) { + // prepare next sub-mission + offsets[current] = offsets[current - 1] + length; + initializer(); + return; + } } + + if (psAlgorithm != null && psState == 0) { + threads = new Thread[]{ + runAsync(1, this::doPostprocessing) + }; + return; + } + + + // this mission is fully finished + + unknownLength = false; + enqueued = false; + running = false; + + deleteThisFromFile(); + notify(DownloadManagerService.MESSAGE_FINISHED); } private void notifyPostProcessing(int state) { @@ -396,10 +396,15 @@ public class DownloadMission extends Mission { Log.d(TAG, action + " postprocessing on " + storage.getName()); + if (state == 2) { + psState = state; + return; + } + synchronized (LOCK) { // don't return without fully write the current state psState = state; - Utility.writeToFile(metadata, DownloadMission.this); + writeThisToFile(); } } @@ -411,12 +416,7 @@ public class DownloadMission extends Mission { if (running || isFinished() || urls.length < 1) return; // ensure that the previous state is completely paused. - int maxWait = 10000;// 10 seconds - joinForThread(init, maxWait); - if (threads != null) { - for (Thread thread : threads) joinForThread(thread, maxWait); - threads = null; - } + joinForThreads(10000); running = true; errCode = ERROR_NOTHING; @@ -427,12 +427,14 @@ public class DownloadMission extends Mission { } if (current >= urls.length) { - runAsync(1, this::notifyFinished); + notifyFinished(); return; } + notify(DownloadManagerService.MESSAGE_RUNNING); + if (urls[current] == null) { - doRecover(null); + doRecover(ERROR_RESOURCE_GONE); return; } @@ -446,18 +448,13 @@ public class DownloadMission extends Mission { blockAcquired = new boolean[blocks.length]; if (blocks.length < 1) { - if (unknownLength) { - done = 0; - length = 0; - } - threads = new Thread[]{runAsync(1, new DownloadRunnableFallback(this))}; } else { int remainingBlocks = 0; for (int block : blocks) if (block >= 0) remainingBlocks++; if (remainingBlocks < 1) { - runAsync(1, this::notifyFinished); + notifyFinished(); return; } @@ -483,6 +480,7 @@ public class DownloadMission extends Mission { } running = false; + notify(DownloadManagerService.MESSAGE_PAUSED); if (init != null && init.isAlive()) { // NOTE: if start() method is running ¡will no have effect! @@ -497,29 +495,14 @@ public class DownloadMission extends Mission { Log.w(TAG, "pausing a download that can not be resumed (range requests not allowed by the server)."); } - // check if the calling thread (alias UI thread) is interrupted - if (Thread.currentThread().isInterrupted()) { - writeThisToFile(); - return; - } - - // wait for all threads are suspended before save the state - if (threads != null) runAsync(-1, this::selfPause); + init = null; + pauseThreads(); } - private void selfPause() { - try { - for (Thread thread : threads) { - if (thread.isAlive()) { - thread.interrupt(); - thread.join(5000); - } - } - } catch (Exception e) { - // nothing to do - } finally { - writeThisToFile(); - } + private void pauseThreads() { + running = false; + joinForThreads(-1); + writeThisToFile(); } /** @@ -527,9 +510,10 @@ public class DownloadMission extends Mission { */ @Override public boolean delete() { - deleted = true; if (psAlgorithm != null) psAlgorithm.cleanupTemporalDir(); + notify(DownloadManagerService.MESSAGE_DELETED); + boolean res = deleteThisFromFile(); if (!super.delete()) return false; @@ -544,35 +528,37 @@ public class DownloadMission extends Mission { * @param persistChanges {@code true} to commit changes to the metadata file, otherwise, {@code false} */ public void resetState(boolean rollback, boolean persistChanges, int errorCode) { - done = 0; + length = 0; errCode = errorCode; errObject = null; unknownLength = false; - threads = null; + threads = new Thread[0]; fallbackResumeOffset = 0; blocks = null; blockAcquired = null; if (rollback) current = 0; - - if (persistChanges) - Utility.writeToFile(metadata, DownloadMission.this); + if (persistChanges) writeThisToFile(); } private void initializer() { init = runAsync(DownloadInitializer.mId, new DownloadInitializer(this)); } + private void writeThisToFileAsync() { + runAsync(-2, this::writeThisToFile); + } + /** * Write this {@link DownloadMission} to the meta file asynchronously * if no thread is already running. */ void writeThisToFile() { synchronized (LOCK) { - if (deleted) return; - Utility.writeToFile(metadata, DownloadMission.this); + if (metadata == null) return; + Utility.writeToFile(metadata, this); + writingToFile = false; } - mWritingToFile = false; } /** @@ -625,11 +611,10 @@ public class DownloadMission extends Mission { public long getLength() { long calculated; if (psState == 1 || psState == 3) { - calculated = length; - } else { - calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length; + return length; } + calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length; calculated -= offsets[0];// don't count reserved space return calculated > nearLength ? calculated : nearLength; @@ -642,7 +627,7 @@ public class DownloadMission extends Mission { */ public void setEnqueued(boolean queue) { enqueued = queue; - runAsync(-2, this::writeThisToFile); + writeThisToFileAsync(); } /** @@ -681,24 +666,19 @@ public class DownloadMission extends Mission { * @return {@code true} if the mission is running a recovery procedure, otherwise, {@code false} */ public boolean isRecovering() { - return threads != null && threads.length > 0 && threads[0] instanceof DownloadRunnable && threads[0].isAlive(); + return threads.length > 0 && threads[0] instanceof DownloadMissionRecover && threads[0].isAlive(); } - private boolean doPostprocessing() { - if (psAlgorithm == null || psState == 2) return true; - + private void doPostprocessing() { + errCode = ERROR_NOTHING; errObject = null; + Thread thread = Thread.currentThread(); notifyPostProcessing(1); - notifyProgress(0); - if (DEBUG) - Thread.currentThread().setName("[" + TAG + "] ps = " + - psAlgorithm.getClass().getSimpleName() + - " filename = " + storage.getName() - ); - - threads = new Thread[]{Thread.currentThread()}; + if (DEBUG) { + thread.setName("[" + TAG + "] ps = " + psAlgorithm + " filename = " + storage.getName()); + } Exception exception = null; @@ -707,6 +687,11 @@ public class DownloadMission extends Mission { } catch (Exception err) { Log.e(TAG, "Post-processing failed. " + psAlgorithm.toString(), err); + if (err instanceof InterruptedIOException || err instanceof ClosedByInterruptException || thread.isInterrupted()) { + notifyError(DownloadMission.ERROR_POSTPROCESSING_STOPPED, null); + return; + } + if (errCode == ERROR_NOTHING) errCode = ERROR_POSTPROCESSING; exception = err; @@ -717,56 +702,38 @@ public class DownloadMission extends Mission { if (errCode != ERROR_NOTHING) { if (exception == null) exception = errObject; notifyError(ERROR_POSTPROCESSING, exception); - - return false; + return; } - return true; + notifyFinished(); } /** * Attempts to recover the download * - * @param fromError exception which require update the url from the source + * @param errorCode error code which trigger the recovery procedure */ - void doRecover(Exception fromError) { + void doRecover(int errorCode) { Log.i(TAG, "Attempting to recover the mission: " + storage.getName()); if (recoveryInfo == null) { - if (fromError == null) - notifyError(ERROR_RESOURCE_GONE, null); - else - notifyError(fromError); - + notifyError(errorCode, null); urls = new String[0];// mark this mission as dead return; } - if (threads != null) { - for (Thread thread : threads) { - if (thread == Thread.currentThread()) continue; - thread.interrupt(); - joinForThread(thread, 0); - } - } - - errCode = ERROR_NOTHING; - errObject = null; - - if (recoveryInfo[current].attempts >= maxRetry) { - recoveryInfo[current].attempts = 0; - notifyError(fromError); - return; - } + joinForThreads(0); threads = new Thread[]{ - runAsync(DownloadMissionRecover.mID, new DownloadMissionRecover(this, fromError)) + runAsync(DownloadMissionRecover.mID, new DownloadMissionRecover(this, errorCode)) }; } private boolean deleteThisFromFile() { synchronized (LOCK) { - return metadata.delete(); + boolean res = metadata.delete(); + metadata = null; + return res; } } @@ -776,8 +743,8 @@ public class DownloadMission extends Mission { * @param id id of new thread (used for debugging only) * @param who the Runnable whose {@code run} method is invoked. */ - private void runAsync(int id, Runnable who) { - runAsync(id, new Thread(who)); + private Thread runAsync(int id, Runnable who) { + return runAsync(id, new Thread(who)); } /** @@ -806,28 +773,44 @@ public class DownloadMission extends Mission { /** * Waits at most {@code millis} milliseconds for the thread to die * - * @param thread the desired thread * @param millis the time to wait in milliseconds */ - private void joinForThread(Thread thread, int millis) { - if (thread == null || !thread.isAlive()) return; - if (thread == Thread.currentThread()) return; + private void joinForThreads(int millis) { + final Thread currentThread = Thread.currentThread(); - if (DEBUG) { - Log.w(TAG, "a thread is !still alive!: " + thread.getName()); + if (init != null && init != currentThread && init.isAlive()) { + init.interrupt(); + + if (millis > 0) { + try { + init.join(millis); + } catch (InterruptedException e) { + Log.w(TAG, "Initializer thread is still running", e); + return; + } + } } - // still alive, this should not happen. - // Possible reasons: + // if a thread is still alive, possible reasons: // slow device // the user is spamming start/pause buttons // start() method called quickly after pause() + for (Thread thread : threads) { + if (!thread.isAlive() || thread == Thread.currentThread()) continue; + thread.interrupt(); + } + try { - thread.join(millis); + for (Thread thread : threads) { + if (!thread.isAlive()) continue; + if (DEBUG) { + Log.w(TAG, "thread alive: " + thread.getName()); + } + if (millis > 0) thread.join(millis); + } } catch (InterruptedException e) { - Log.d(TAG, "timeout on join : " + thread.getName()); - throw new RuntimeException("A thread is still running:\n" + thread.getName()); + throw new RuntimeException("A download thread is still running", e); } } diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java b/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java index 5efbd1153..eb660e564 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java @@ -4,6 +4,7 @@ import android.util.Log; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamExtractor; import org.schabi.newpipe.extractor.stream.SubtitlesStream; @@ -15,7 +16,8 @@ import java.net.HttpURLConnection; import java.nio.channels.ClosedByInterruptException; import java.util.List; -import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING; +import us.shandian.giga.get.DownloadMission.HttpError; + import static us.shandian.giga.get.DownloadMission.ERROR_RESOURCE_GONE; public class DownloadMissionRecover extends Thread { @@ -23,47 +25,67 @@ public class DownloadMissionRecover extends Thread { static final int mID = -3; private final DownloadMission mMission; - private final Exception mFromError; - private final boolean notInitialized; + private final boolean mNotInitialized; + + private final int mErrCode; private HttpURLConnection mConn; private MissionRecoveryInfo mRecovery; private StreamExtractor mExtractor; - DownloadMissionRecover(DownloadMission mission, Exception originError) { + DownloadMissionRecover(DownloadMission mission, int errCode) { mMission = mission; - mFromError = originError; - notInitialized = mission.blocks == null && mission.current == 0; + mNotInitialized = mission.blocks == null && mission.current == 0; + mErrCode = errCode; } @Override public void run() { if (mMission.source == null) { - mMission.notifyError(mFromError); + mMission.notifyError(mErrCode, null); return; } + Exception err = null; + int attempt = 0; + + while (attempt++ < mMission.maxRetry) { + try { + tryRecover(); + return; + } catch (InterruptedIOException | ClosedByInterruptException e) { + return; + } catch (Exception e) { + if (!mMission.running || super.isInterrupted()) return; + err = e; + } + } + + // give up + mMission.notifyError(mErrCode, err); + } + + private void tryRecover() throws ExtractionException, IOException, HttpError { /*if (mMission.source.startsWith(MissionRecoveryInfo.DIRECT_SOURCE)) { resolve(mMission.source.substring(MissionRecoveryInfo.DIRECT_SOURCE.length())); return; }*/ - try { - StreamingService svr = NewPipe.getServiceByUrl(mMission.source); - mExtractor = svr.getStreamExtractor(mMission.source); - mExtractor.fetchPage(); - } catch (InterruptedIOException | ClosedByInterruptException e) { - return; - } catch (Exception e) { - if (!mMission.running || super.isInterrupted()) return; - mMission.notifyError(e); - return; + if (mExtractor == null) { + try { + StreamingService svr = NewPipe.getServiceByUrl(mMission.source); + mExtractor = svr.getStreamExtractor(mMission.source); + mExtractor.fetchPage(); + } catch (ExtractionException e) { + mExtractor = null; + throw e; + } } // maybe the following check is redundant if (!mMission.running || super.isInterrupted()) return; - if (!notInitialized) { + if (!mNotInitialized) { // set the current download url to null in case if the recovery // process is canceled. Next time start() method is called the // recovery will be executed, saving time @@ -87,7 +109,7 @@ public class DownloadMissionRecover extends Thread { if (!mMission.running) return; // before continue, check if the current stream was resolved - if (mMission.urls[mMission.current] == null || mMission.errCode != ERROR_NOTHING) { + if (mMission.urls[mMission.current] == null) { break; } } @@ -103,59 +125,54 @@ public class DownloadMissionRecover extends Thread { mMission.start(); } - private void resolveStream() { - if (mExtractor.getErrorMessage() != null) { - mMission.notifyError(mFromError); + private void resolveStream() throws IOException, ExtractionException, HttpError { + // FIXME: this getErrorMessage() always returns "video is unavailable" + /*if (mExtractor.getErrorMessage() != null) { + mMission.notifyError(mErrCode, new ExtractionException(mExtractor.getErrorMessage())); return; + }*/ + + String url = null; + + switch (mRecovery.kind) { + case 'a': + for (AudioStream audio : mExtractor.getAudioStreams()) { + if (audio.average_bitrate == mRecovery.desiredBitrate && audio.getFormat() == mRecovery.format) { + url = audio.getUrl(); + break; + } + } + break; + case 'v': + List videoStreams; + if (mRecovery.desired2) + videoStreams = mExtractor.getVideoOnlyStreams(); + else + videoStreams = mExtractor.getVideoStreams(); + for (VideoStream video : videoStreams) { + if (video.resolution.equals(mRecovery.desired) && video.getFormat() == mRecovery.format) { + url = video.getUrl(); + break; + } + } + break; + case 's': + for (SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery.format)) { + String tag = subtitles.getLanguageTag(); + if (tag.equals(mRecovery.desired) && subtitles.isAutoGenerated() == mRecovery.desired2) { + url = subtitles.getURL(); + break; + } + } + break; + default: + throw new RuntimeException("Unknown stream type"); } - try { - String url = null; - - switch (mRecovery.kind) { - case 'a': - for (AudioStream audio : mExtractor.getAudioStreams()) { - if (audio.average_bitrate == mRecovery.desiredBitrate && audio.getFormat() == mRecovery.format) { - url = audio.getUrl(); - break; - } - } - break; - case 'v': - List videoStreams; - if (mRecovery.desired2) - videoStreams = mExtractor.getVideoOnlyStreams(); - else - videoStreams = mExtractor.getVideoStreams(); - for (VideoStream video : videoStreams) { - if (video.resolution.equals(mRecovery.desired) && video.getFormat() == mRecovery.format) { - url = video.getUrl(); - break; - } - } - break; - case 's': - for (SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery.format)) { - String tag = subtitles.getLanguageTag(); - if (tag.equals(mRecovery.desired) && subtitles.isAutoGenerated() == mRecovery.desired2) { - url = subtitles.getURL(); - break; - } - } - break; - default: - throw new RuntimeException("Unknown stream type"); - } - - resolve(url); - } catch (Exception e) { - if (!mMission.running || e instanceof ClosedByInterruptException) return; - mRecovery.attempts++; - mMission.notifyError(e); - } + resolve(url); } - private void resolve(String url) throws IOException, DownloadMission.HttpError { + private void resolve(String url) throws IOException, HttpError { if (mRecovery.validateCondition == null) { Log.w(TAG, "validation condition not defined, the resource can be stale"); } @@ -190,10 +207,7 @@ public class DownloadMissionRecover extends Thread { return; } - throw new DownloadMission.HttpError(code); - } catch (Exception e) { - if (!mMission.running || e instanceof ClosedByInterruptException) return; - throw e; + throw new HttpError(code); } finally { disconnect(); } @@ -205,14 +219,14 @@ public class DownloadMissionRecover extends Thread { ); mMission.urls[mMission.current] = url; - mRecovery.attempts = 0; if (url == null) { + mMission.urls = new String[0]; mMission.notifyError(ERROR_RESOURCE_GONE, null); return; } - if (notInitialized) return; + if (mNotInitialized) return; if (stale) { mMission.resetState(false, false, DownloadMission.ERROR_NOTHING); diff --git a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java index b0dc793bc..4aa6e912e 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java @@ -87,6 +87,7 @@ public class DownloadRunnable extends Thread { if (mConn.getResponseCode() == 416) { if (block.done > 0) { // try again from the start (of the block) + mMission.notifyProgress(-block.done); block.done = 0; retry = true; mConn.disconnect(); @@ -114,7 +115,7 @@ public class DownloadRunnable extends Thread { int len; // use always start <= end - // fixes a deadlock in DownloadRunnable because youtube is sending one byte alone after downloading 26MiB exactly + // fixes a deadlock because in some videos, youtube is sending one byte alone while (start <= end && mMission.running && (len = is.read(buf, 0, buf.length)) != -1) { f.write(buf, 0, len); start += len; @@ -135,7 +136,7 @@ public class DownloadRunnable extends Thread { if (mId == 1) { // only the first thread will execute the recovery procedure - mMission.doRecover(e); + mMission.doRecover(ERROR_HTTP_FORBIDDEN); } return; } diff --git a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java index e64322b48..9cb40cb32 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java @@ -1,8 +1,9 @@ package us.shandian.giga.get; -import androidx.annotation.NonNull; import android.util.Log; +import androidx.annotation.NonNull; + import org.schabi.newpipe.streams.io.SharpStream; import java.io.IOException; @@ -47,22 +48,10 @@ public class DownloadRunnableFallback extends Thread { if (mF != null) mF.close(); } - private long loadPosition() { - synchronized (mMission.LOCK) { - return mMission.fallbackResumeOffset; - } - } - - private void savePosition(long position) { - synchronized (mMission.LOCK) { - mMission.fallbackResumeOffset = position; - } - } - @Override public void run() { boolean done; - long start = loadPosition(); + long start = mMission.fallbackResumeOffset; if (DEBUG && !mMission.unknownLength && start > 0) { Log.i(TAG, "Resuming a single-thread download at " + start); @@ -83,6 +72,7 @@ public class DownloadRunnableFallback extends Thread { // check if the download can be resumed if (mConn.getResponseCode() == 416 && start > 0) { + mMission.notifyProgress(-start); start = 0; mRetryCount--; throw new DownloadMission.HttpError(416); @@ -92,6 +82,11 @@ public class DownloadRunnableFallback extends Thread { if (!mMission.unknownLength) mMission.unknownLength = Utility.getContentLength(mConn) == -1; + if (mMission.unknownLength || mConn.getResponseCode() == 200) { + // restart amount of bytes downloaded + mMission.done = mMission.offsets[mMission.current] - mMission.offsets[0]; + } + mF = mMission.storage.getStream(); mF.seek(mMission.offsets[mMission.current] + start); @@ -113,14 +108,14 @@ public class DownloadRunnableFallback extends Thread { } catch (Exception e) { dispose(); - savePosition(start); + mMission.fallbackResumeOffset = start; if (!mMission.running || e instanceof ClosedByInterruptException) return; if (e instanceof HttpError && ((HttpError) e).statusCode == ERROR_HTTP_FORBIDDEN) { // for youtube streams. The url has expired, recover dispose(); - mMission.doRecover(e); + mMission.doRecover(ERROR_HTTP_FORBIDDEN); return; } @@ -140,7 +135,7 @@ public class DownloadRunnableFallback extends Thread { if (done) { mMission.notifyFinished(); } else { - savePosition(start); + mMission.fallbackResumeOffset = start; } } diff --git a/app/src/main/java/us/shandian/giga/get/FinishedMission.java b/app/src/main/java/us/shandian/giga/get/FinishedMission.java index b468f3c76..6bc5423b8 100644 --- a/app/src/main/java/us/shandian/giga/get/FinishedMission.java +++ b/app/src/main/java/us/shandian/giga/get/FinishedMission.java @@ -2,17 +2,17 @@ package us.shandian.giga.get; import androidx.annotation.NonNull; -public class FinishedMission extends Mission { +public class FinishedMission extends Mission { public FinishedMission() { } public FinishedMission(@NonNull DownloadMission mission) { source = mission.source; - length = mission.length;// ¿or mission.done? + length = mission.length; timestamp = mission.timestamp; kind = mission.kind; storage = mission.storage; - } + } diff --git a/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.java b/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.java index bd1d9bc49..f6a3a3984 100644 --- a/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.java +++ b/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.java @@ -2,7 +2,8 @@ package us.shandian.giga.get; import android.os.Parcel; import android.os.Parcelable; -import android.support.annotation.NonNull; + +import androidx.annotation.NonNull; import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.stream.AudioStream; @@ -23,8 +24,6 @@ public class MissionRecoveryInfo implements Serializable, Parcelable { byte kind; String validateCondition = null; - transient int attempts = 0; - public MissionRecoveryInfo(@NonNull Stream stream) { if (stream instanceof AudioStream) { desiredBitrate = ((AudioStream) stream).average_bitrate; @@ -51,7 +50,7 @@ public class MissionRecoveryInfo implements Serializable, Parcelable { public String toString() { String info; StringBuilder str = new StringBuilder(); - str.append("type="); + str.append("{type="); switch (kind) { case 'a': str.append("audio"); @@ -73,7 +72,8 @@ public class MissionRecoveryInfo implements Serializable, Parcelable { str.append(" format=") .append(format.getName()) .append(' ') - .append(info); + .append(info) + .append('}'); return str.toString(); } diff --git a/app/src/main/java/us/shandian/giga/io/ChunkFileInputStream.java b/app/src/main/java/us/shandian/giga/io/ChunkFileInputStream.java index 16a90fcee..98015e37e 100644 --- a/app/src/main/java/us/shandian/giga/io/ChunkFileInputStream.java +++ b/app/src/main/java/us/shandian/giga/io/ChunkFileInputStream.java @@ -5,21 +5,23 @@ import org.schabi.newpipe.streams.io.SharpStream; import java.io.IOException; public class ChunkFileInputStream extends SharpStream { + private static final int REPORT_INTERVAL = 256 * 1024; private SharpStream source; private final long offset; private final long length; private long position; - public ChunkFileInputStream(SharpStream target, long start) throws IOException { - this(target, start, target.length()); - } + private long progressReport; + private final ProgressReport onProgress; - public ChunkFileInputStream(SharpStream target, long start, long end) throws IOException { + public ChunkFileInputStream(SharpStream target, long start, long end, ProgressReport callback) throws IOException { source = target; offset = start; length = end - start; position = 0; + onProgress = callback; + progressReport = REPORT_INTERVAL; if (length < 1) { source.close(); @@ -60,12 +62,12 @@ public class ChunkFileInputStream extends SharpStream { } @Override - public int read(byte b[]) throws IOException { + public int read(byte[] b) throws IOException { return read(b, 0, b.length); } @Override - public int read(byte b[], int off, int len) throws IOException { + public int read(byte[] b, int off, int len) throws IOException { if ((position + len) > length) { len = (int) (length - position); } @@ -76,6 +78,11 @@ public class ChunkFileInputStream extends SharpStream { int res = source.read(b, off, len); position += res; + if (onProgress != null && position > progressReport) { + onProgress.report(position); + progressReport = position + REPORT_INTERVAL; + } + return res; } diff --git a/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java b/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java index e2afb9202..102580570 100644 --- a/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java +++ b/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java @@ -174,12 +174,12 @@ public class CircularFileWriter extends SharpStream { } @Override - public void write(byte b[]) throws IOException { + public void write(byte[] b) throws IOException { write(b, 0, b.length); } @Override - public void write(byte b[], int off, int len) throws IOException { + public void write(byte[] b, int off, int len) throws IOException { if (len == 0) { return; } @@ -261,7 +261,7 @@ public class CircularFileWriter extends SharpStream { @Override public void rewind() throws IOException { if (onProgress != null) { - onProgress.report(-out.length - aux.length);// rollback the whole progress + onProgress.report(0);// rollback the whole progress } seek(0); @@ -357,16 +357,6 @@ public class CircularFileWriter extends SharpStream { long check(); } - public interface ProgressReport { - - /** - * Report the size of the new file - * - * @param progress the new size - */ - void report(long progress); - } - public interface WriteErrorHandle { /** @@ -381,10 +371,10 @@ public class CircularFileWriter extends SharpStream { class BufferedFile { - protected final SharpStream target; + final SharpStream target; private long offset; - protected long length; + long length; private byte[] queue = new byte[QUEUE_BUFFER_SIZE]; private int queueSize; @@ -397,16 +387,16 @@ public class CircularFileWriter extends SharpStream { this.target = target; } - protected long getOffset() { + long getOffset() { return offset + queueSize;// absolute offset in the file } - protected void close() { + void close() { queue = null; target.close(); } - protected void write(byte b[], int off, int len) throws IOException { + void write(byte[] b, int off, int len) throws IOException { while (len > 0) { // if the queue is full, the method available() will flush the queue int read = Math.min(available(), len); @@ -436,7 +426,7 @@ public class CircularFileWriter extends SharpStream { target.seek(0); } - protected int available() throws IOException { + int available() throws IOException { if (queueSize >= queue.length) { flush(); return queue.length; @@ -451,7 +441,7 @@ public class CircularFileWriter extends SharpStream { target.seek(0); } - protected void seek(long absoluteOffset) throws IOException { + void seek(long absoluteOffset) throws IOException { if (absoluteOffset == offset) { return;// nothing to do } diff --git a/app/src/main/java/us/shandian/giga/io/ProgressReport.java b/app/src/main/java/us/shandian/giga/io/ProgressReport.java new file mode 100644 index 000000000..14ae9ded9 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/io/ProgressReport.java @@ -0,0 +1,11 @@ +package us.shandian.giga.io; + +public interface ProgressReport { + + /** + * Report the size of the new file + * + * @param progress the new size + */ + void report(long progress); +} \ No newline at end of file diff --git a/app/src/main/java/us/shandian/giga/postprocessing/OggFromWebmDemuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/OggFromWebmDemuxer.java index 605c0a88b..04958c495 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/OggFromWebmDemuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/OggFromWebmDemuxer.java @@ -1,6 +1,6 @@ package us.shandian.giga.postprocessing; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; import org.schabi.newpipe.streams.OggFromWebMWriter; import org.schabi.newpipe.streams.io.SharpStream; diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java index 92510c3df..773ff92d1 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java @@ -1,9 +1,9 @@ package us.shandian.giga.postprocessing; -import android.os.Message; -import androidx.annotation.NonNull; import android.util.Log; +import androidx.annotation.NonNull; + import org.schabi.newpipe.streams.io.SharpStream; import java.io.File; @@ -14,11 +14,11 @@ import us.shandian.giga.get.DownloadMission; import us.shandian.giga.io.ChunkFileInputStream; import us.shandian.giga.io.CircularFileWriter; import us.shandian.giga.io.CircularFileWriter.OffsetChecker; -import us.shandian.giga.service.DownloadManagerService; +import us.shandian.giga.io.ProgressReport; import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING; +import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING; import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD; -import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; public abstract class Postprocessing implements Serializable { @@ -63,22 +63,22 @@ public abstract class Postprocessing implements Serializable { * Get a boolean value that indicate if the given algorithm work on the same * file */ - public final boolean worksOnSameFile; + public boolean worksOnSameFile; /** * Indicates whether the selected algorithm needs space reserved at the beginning of the file */ - public final boolean reserveSpace; + public boolean reserveSpace; /** * Gets the given algorithm short name */ - private final String name; + private String name; private String[] args; - protected transient DownloadMission mission; + private transient DownloadMission mission; private File tempFile; @@ -109,16 +109,24 @@ public abstract class Postprocessing implements Serializable { long finalLength = -1; mission.done = 0; - mission.length = mission.storage.length(); + + long length = mission.storage.length() - mission.offsets[0]; + mission.length = length > mission.nearLength ? length : mission.nearLength; + + final ProgressReport readProgress = (long position) -> { + position -= mission.offsets[0]; + if (position > mission.done) mission.done = position; + }; if (worksOnSameFile) { ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length]; try { - int i = 0; - for (; i < sources.length - 1; i++) { - sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i], mission.offsets[i + 1]); + for (int i = 0, j = 1; i < sources.length; i++, j++) { + SharpStream source = mission.storage.getStream(); + long end = j < sources.length ? mission.offsets[j] : source.length(); + + sources[i] = new ChunkFileInputStream(source, mission.offsets[i], end, readProgress); } - sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i]); if (test(sources)) { for (SharpStream source : sources) source.rewind(); @@ -140,7 +148,7 @@ public abstract class Postprocessing implements Serializable { }; out = new CircularFileWriter(mission.storage.getStream(), tempFile, checker); - out.onProgress = this::progressReport; + out.onProgress = (long position) -> mission.done = position; out.onWriteError = (err) -> { mission.psState = 3; @@ -187,11 +195,10 @@ public abstract class Postprocessing implements Serializable { if (result == OK_RESULT) { if (finalLength != -1) { - mission.done = finalLength; mission.length = finalLength; } } else { - mission.errCode = ERROR_UNKNOWN_EXCEPTION; + mission.errCode = ERROR_POSTPROCESSING; mission.errObject = new RuntimeException("post-processing algorithm returned " + result); } @@ -229,23 +236,12 @@ public abstract class Postprocessing implements Serializable { return args[index]; } - private void progressReport(long done) { - mission.done = done; - if (mission.length < mission.done) mission.length = mission.done; - - Message m = new Message(); - m.what = DownloadManagerService.MESSAGE_PROGRESS; - m.obj = mission; - - mission.mHandler.sendMessage(m); - } - @NonNull @Override public String toString() { StringBuilder str = new StringBuilder(); - str.append("name=").append(name).append('['); + str.append("{ name=").append(name).append('['); if (args != null) { for (String arg : args) { @@ -255,6 +251,6 @@ public abstract class Postprocessing implements Serializable { str.delete(0, 1); } - return str.append(']').toString(); + return str.append("] }").toString(); } } diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java index 2d1e9cd00..e8bc468e9 100644 --- a/app/src/main/java/us/shandian/giga/service/DownloadManager.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java @@ -2,13 +2,11 @@ package us.shandian.giga.service; import android.content.Context; import android.os.Handler; +import android.util.Log; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.DiffUtil; -import android.util.Log; -import android.widget.Toast; - -import org.schabi.newpipe.R; import java.io.File; import java.io.IOException; @@ -152,6 +150,8 @@ public class DownloadManager { continue; } + mis.threads = new Thread[0]; + boolean exists; try { mis.storage = StoredFileHelper.deserialize(mis.storage, ctx); @@ -170,8 +170,6 @@ public class DownloadManager { // is Java IO (avoid showing the "Save as..." dialog) if (exists && mis.storage.isDirect() && !mis.storage.delete()) Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath()); - - exists = true; } mis.psState = 0; @@ -243,7 +241,6 @@ public class DownloadManager { boolean start = !mPrefQueueLimit || getRunningMissionsCount() < 1; if (canDownloadInCurrentNetwork() && start) { - mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS); mission.start(); } } @@ -252,7 +249,6 @@ public class DownloadManager { public void resumeMission(DownloadMission mission) { if (!mission.running) { - mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS); mission.start(); } } @@ -261,7 +257,6 @@ public class DownloadManager { if (mission.running) { mission.setEnqueued(false); mission.pause(); - mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED); } } @@ -274,7 +269,6 @@ public class DownloadManager { mFinishedMissionStore.deleteMission(mission); } - mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED); mission.delete(); } } @@ -291,7 +285,6 @@ public class DownloadManager { mFinishedMissionStore.deleteMission(mission); } - mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED); mission.storage = null; mission.delete(); } @@ -374,35 +367,29 @@ public class DownloadManager { } 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 + if (force) { + // avoid waiting for threads + mission.init = null; + mission.threads = new Thread[0]; + } 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.isCorrupt()) continue; - flag = true; mission.start(); } } - - if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS); } /** @@ -483,28 +470,18 @@ public class DownloadManager { boolean isMetered = mPrefMeteredDownloads && mLastNetworkStatus == NetworkState.MeteredOperating; - int running = 0; - int paused = 0; synchronized (this) { for (DownloadMission mission : mMissionsPending) { if (mission.isCorrupt() || 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() { @@ -513,22 +490,6 @@ public class DownloadManager { } } - /** - * 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); diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index ea9029c0b..3da0e75b8 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -25,14 +25,15 @@ import android.os.IBinder; import android.os.Message; import android.os.Parcelable; import android.preference.PreferenceManager; +import android.util.Log; +import android.util.SparseArray; +import android.widget.Toast; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat.Builder; -import android.util.Log; -import android.util.SparseArray; -import android.widget.Toast; import org.schabi.newpipe.R; import org.schabi.newpipe.download.DownloadActivity; @@ -41,8 +42,6 @@ import org.schabi.newpipe.player.helper.LockManager; import java.io.File; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.MissionRecoveryInfo; @@ -58,11 +57,11 @@ public class DownloadManagerService extends Service { private static final String TAG = "DownloadManagerService"; + public static final int MESSAGE_RUNNING = 0; public static final int MESSAGE_PAUSED = 1; public static final int MESSAGE_FINISHED = 2; - public static final int MESSAGE_PROGRESS = 3; - public static final int MESSAGE_ERROR = 4; - public static final int MESSAGE_DELETED = 5; + public static final int MESSAGE_ERROR = 3; + public static final int MESSAGE_DELETED = 4; private static final int FOREGROUND_NOTIFICATION_ID = 1000; private static final int DOWNLOADS_NOTIFICATION_ID = 1001; @@ -217,9 +216,11 @@ public class DownloadManagerService extends Service { .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) ); } + return START_NOT_STICKY; } } - return START_NOT_STICKY; + + return START_STICKY; } @Override @@ -250,6 +251,7 @@ public class DownloadManagerService extends Service { if (icDownloadFailed != null) icDownloadFailed.recycle(); if (icLauncher != null) icLauncher.recycle(); + mHandler = null; mManager.pauseAllMissions(true); } @@ -274,6 +276,8 @@ public class DownloadManagerService extends Service { } private boolean handleMessage(@NonNull Message msg) { + if (mHandler == null) return true; + DownloadMission mission = (DownloadMission) msg.obj; switch (msg.what) { @@ -284,7 +288,7 @@ public class DownloadManagerService extends Service { handleConnectivityState(false); updateForegroundState(mManager.runMissions()); break; - case MESSAGE_PROGRESS: + case MESSAGE_RUNNING: updateForegroundState(true); break; case MESSAGE_ERROR: @@ -300,11 +304,8 @@ public class DownloadManagerService extends Service { if (msg.what != MESSAGE_ERROR) mFailedDownloads.delete(mFailedDownloads.indexOfValue(mission)); - synchronized (mEchoObservers) { - for (Callback observer : mEchoObservers) { - observer.handleMessage(msg); - } - } + for (Callback observer : mEchoObservers) + observer.handleMessage(msg); return true; } @@ -523,16 +524,6 @@ public class DownloadManagerService extends Service { return PendingIntent.getService(this, intent.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT); } - private void manageObservers(Callback handler, boolean add) { - synchronized (mEchoObservers) { - if (add) { - mEchoObservers.add(handler); - } else { - mEchoObservers.remove(handler); - } - } - } - private void manageLock(boolean acquire) { if (acquire == mLockAcquired) return; @@ -605,11 +596,11 @@ public class DownloadManagerService extends Service { } public void addMissionEventListener(Callback handler) { - manageObservers(handler, true); + mEchoObservers.add(handler); } public void removeMissionEventListener(Callback handler) { - manageObservers(handler, false); + mEchoObservers.remove(handler); } public void clearDownloadNotifications() { diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index 78fd7ea9d..e3a7f112a 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -10,16 +10,6 @@ import android.os.AsyncTask; import android.os.Build; import android.os.Handler; import android.os.Message; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.core.content.FileProvider; -import androidx.core.view.ViewCompat; -import androidx.appcompat.app.AlertDialog; -import androidx.recyclerview.widget.DiffUtil; -import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.RecyclerView.Adapter; -import androidx.recyclerview.widget.RecyclerView.ViewHolder; import android.util.Log; import android.util.SparseArray; import android.view.HapticFeedbackConstants; @@ -34,6 +24,17 @@ import android.widget.PopupMenu; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.FileProvider; +import androidx.core.view.ViewCompat; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView.Adapter; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; + import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.NewPipe; @@ -82,6 +83,7 @@ public class MissionAdapter extends Adapter implements Handler.Callb private static final String TAG = "MissionAdapter"; private static final String UNDEFINED_PROGRESS = "--.-%"; private static final String DEFAULT_MIME_TYPE = "*/*"; + private static final String UNDEFINED_ETA = "--:--"; static { @@ -103,10 +105,11 @@ public class MissionAdapter extends Adapter implements Handler.Callb private View mEmptyMessage; private RecoverHelper mRecover; - public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage) { + private final Runnable rUpdater = this::updater; + + public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage, View root) { mContext = context; mDownloadManager = downloadManager; - mDeleter = null; mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mLayout = R.layout.mission_item; @@ -117,7 +120,10 @@ public class MissionAdapter extends Adapter implements Handler.Callb mIterator = downloadManager.getIterator(); + mDeleter = new Deleter(root, mContext, this, mDownloadManager, mIterator, mHandler); + checkEmptyMessageVisibility(); + onResume(); } @Override @@ -142,17 +148,13 @@ public class MissionAdapter extends Adapter implements Handler.Callb if (h.item.mission instanceof DownloadMission) { mPendingDownloadsItems.remove(h); if (mPendingDownloadsItems.size() < 1) { - setAutoRefresh(false); checkMasterButtonsVisibility(); } } h.popupMenu.dismiss(); h.item = null; - h.lastTimeStamp = -1; - h.lastDone = -1; - h.lastCurrent = -1; - h.state = 0; + h.resetSpeedMeasure(); } @Override @@ -191,7 +193,6 @@ public class MissionAdapter extends Adapter implements Handler.Callb h.size.setText(length); h.pause.setTitle(mission.unknownLength ? R.string.stop : R.string.pause); - h.lastCurrent = mission.current; updateProgress(h); mPendingDownloadsItems.add(h); } else { @@ -216,20 +217,10 @@ public class MissionAdapter extends Adapter implements Handler.Callb private void updateProgress(ViewHolderItem h) { if (h == null || h.item == null || h.item.mission instanceof FinishedMission) return; - long now = System.currentTimeMillis(); DownloadMission mission = (DownloadMission) h.item.mission; - - if (h.lastCurrent != mission.current) { - h.lastCurrent = mission.current; - h.lastTimeStamp = now; - h.lastDone = 0; - } else { - if (h.lastTimeStamp == -1) h.lastTimeStamp = now; - if (h.lastDone == -1) h.lastDone = mission.done; - } - - long deltaTime = now - h.lastTimeStamp; - long deltaDone = mission.done - h.lastDone; + double done = mission.done; + long length = mission.getLength(); + long now = System.currentTimeMillis(); boolean hasError = mission.errCode != ERROR_NOTHING; // hide on error @@ -237,19 +228,16 @@ public class MissionAdapter extends Adapter implements Handler.Callb // show if length is unknown h.progress.setMarquee(mission.isRecovering() || !hasError && (!mission.isInitialized() || mission.unknownLength)); - float progress; + double progress; if (mission.unknownLength) { - progress = Float.NaN; + progress = Double.NaN; h.progress.setProgress(0f); } else { - progress = (float) ((double) mission.done / mission.length); - if (mission.urls.length > 1 && mission.current < mission.urls.length) { - progress = (progress / mission.urls.length) + ((float) mission.current / mission.urls.length); - } + progress = done / length; } if (hasError) { - h.progress.setProgress(isNotFinite(progress) ? 1f : progress); + h.progress.setProgress(isNotFinite(progress) ? 1d : progress); h.status.setText(R.string.msg_error); } else if (isNotFinite(progress)) { h.status.setText(UNDEFINED_PROGRESS); @@ -258,59 +246,78 @@ public class MissionAdapter extends Adapter implements Handler.Callb h.progress.setProgress(progress); } - long length = mission.getLength(); + @StringRes int state; + String sizeStr = Utility.formatBytes(length).concat(" "); - int state; if (mission.isPsFailed() || mission.errCode == ERROR_POSTPROCESSING_HOLD) { - state = 0; + h.size.setText(sizeStr); + return; } else if (!mission.running) { - state = mission.enqueued ? 1 : 2; + state = mission.enqueued ? R.string.queued : R.string.paused; } else if (mission.isPsRunning()) { - state = 3; + state = R.string.post_processing; + } else if (mission.isRecovering()) { + state = R.string.recovering; } else { state = 0; } if (state != 0) { // update state without download speed - if (h.state != state) { - String statusStr; - h.state = state; + h.size.setText(sizeStr.concat("(").concat(mContext.getString(state)).concat(")")); + h.resetSpeedMeasure(); + return; + } - switch (state) { - case 1: - statusStr = mContext.getString(R.string.queued); - break; - case 2: - statusStr = mContext.getString(R.string.paused); - break; - case 3: - statusStr = mContext.getString(R.string.post_processing); - break; - default: - statusStr = "?"; - break; - } + if (h.lastTimestamp < 0) { + h.size.setText(sizeStr); + h.lastTimestamp = now; + h.lastDone = done; + return; + } - h.size.setText(Utility.formatBytes(length).concat(" (").concat(statusStr).concat(")")); - } else if (deltaDone > 0) { - h.lastTimeStamp = now; - h.lastDone = mission.done; - } + long deltaTime = now - h.lastTimestamp; + double deltaDone = done - h.lastDone; + if (h.lastDone > done) { + h.lastDone = done; + h.size.setText(sizeStr); return; } if (deltaDone > 0 && deltaTime > 0) { - float speed = (deltaDone * 1000f) / deltaTime; + float speed = (float) ((deltaDone * 1000d) / deltaTime); + float averageSpeed = speed; - String speedStr = Utility.formatSpeed(speed); - String sizeStr = Utility.formatBytes(length); + if (h.lastSpeedIdx < 0) { + for (int i = 0; i < h.lastSpeed.length; i++) { + h.lastSpeed[i] = speed; + } + h.lastSpeedIdx = 0; + } else { + for (int i = 0; i < h.lastSpeed.length; i++) { + averageSpeed += h.lastSpeed[i]; + } + averageSpeed /= h.lastSpeed.length + 1f; + } - h.size.setText(sizeStr.concat(" ").concat(speedStr)); + String speedStr = Utility.formatSpeed(averageSpeed); + String etaStr; - h.lastTimeStamp = now; - h.lastDone = mission.done; + if (mission.unknownLength) { + etaStr = ""; + } else { + long eta = (long) Math.ceil((length - done) / averageSpeed); + etaStr = " @ ".concat(Utility.stringifySeconds(eta)); + } + + h.size.setText(sizeStr.concat(speedStr).concat(etaStr)); + + h.lastTimestamp = now; + h.lastDone = done; + h.lastSpeed[h.lastSpeedIdx++] = speed; + + if (h.lastSpeedIdx >= h.lastSpeed.length) h.lastSpeedIdx = 0; } } @@ -389,6 +396,13 @@ public class MissionAdapter extends Adapter implements Handler.Callb return true; } + private ViewHolderItem getViewHolder(Object mission) { + for (ViewHolderItem h : mPendingDownloadsItems) { + if (h.item.mission == mission) return h; + } + return null; + } + @Override public boolean handleMessage(@NonNull Message msg) { if (mStartButton != null && mPauseButton != null) { @@ -396,33 +410,28 @@ public class MissionAdapter extends Adapter implements Handler.Callb } switch (msg.what) { - case DownloadManagerService.MESSAGE_PROGRESS: case DownloadManagerService.MESSAGE_ERROR: case DownloadManagerService.MESSAGE_FINISHED: + case DownloadManagerService.MESSAGE_DELETED: + case DownloadManagerService.MESSAGE_PAUSED: break; default: return false; } - if (msg.what == DownloadManagerService.MESSAGE_PROGRESS) { - setAutoRefresh(true); - return true; - } + ViewHolderItem h = getViewHolder(msg.obj); + if (h == null) return false; - for (ViewHolderItem h : mPendingDownloadsItems) { - if (h.item.mission != msg.obj) continue; - - if (msg.what == DownloadManagerService.MESSAGE_FINISHED) { + switch (msg.what) { + case DownloadManagerService.MESSAGE_FINISHED: + case DownloadManagerService.MESSAGE_DELETED: // DownloadManager should mark the download as finished applyChanges(); return true; - } - - updateProgress(h); - return true; } - return false; + updateProgress(h); + return true; } private void showError(@NonNull DownloadMission mission) { @@ -470,8 +479,13 @@ public class MissionAdapter extends Adapter implements Handler.Callb msg = R.string.error_insufficient_storage; break; case ERROR_UNKNOWN_EXCEPTION: - showError(mission, UserAction.DOWNLOAD_FAILED, R.string.general_error); - return; + if (mission.errObject != null) { + showError(mission, UserAction.DOWNLOAD_FAILED, R.string.general_error); + return; + } else { + msg = R.string.msg_error; + break; + } case ERROR_PROGRESS_LOST: msg = R.string.error_progress_lost; break; @@ -521,7 +535,9 @@ public class MissionAdapter extends Adapter implements Handler.Callb request.append(" ["); if (mission.recoveryInfo != null) { for (MissionRecoveryInfo recovery : mission.recoveryInfo) - request.append(" {").append(recovery.toString()).append("} "); + request.append(' ') + .append(recovery.toString()) + .append(' '); } request.append("]"); @@ -556,16 +572,10 @@ public class MissionAdapter extends Adapter implements Handler.Callb switch (id) { case R.id.start: h.status.setText(UNDEFINED_PROGRESS); - h.state = -1; - h.size.setText(Utility.formatBytes(mission.getLength())); mDownloadManager.resumeMission(mission); return true; case R.id.pause: - h.state = -1; mDownloadManager.pauseMission(mission); - updateProgress(h); - h.lastTimeStamp = -1; - h.lastDone = -1; return true; case R.id.error_message_view: showError(mission); @@ -598,12 +608,9 @@ public class MissionAdapter extends Adapter implements Handler.Callb shareFile(h.item.mission); return true; case R.id.delete: - if (mDeleter == null) { - mDownloadManager.deleteMission(h.item.mission); - } else { - mDeleter.append(h.item.mission); - } + mDeleter.append(h.item.mission); applyChanges(); + checkMasterButtonsVisibility(); return true; case R.id.md5: case R.id.sha1: @@ -639,7 +646,7 @@ public class MissionAdapter extends Adapter implements Handler.Callb mIterator.end(); for (ViewHolderItem item : mPendingDownloadsItems) { - item.lastTimeStamp = -1; + item.resetSpeedMeasure(); } notifyDataSetChanged(); @@ -672,6 +679,7 @@ public class MissionAdapter extends Adapter implements Handler.Callb public void checkMasterButtonsVisibility() { boolean[] state = mIterator.hasValidPendingMissions(); + Log.d(TAG, "checkMasterButtonsVisibility() running=" + state[0] + " paused=" + state[1]); setButtonVisible(mPauseButton, state[0]); setButtonVisible(mStartButton, state[1]); } @@ -681,86 +689,57 @@ public class MissionAdapter extends Adapter implements Handler.Callb button.setVisible(visible); } - public void ensurePausedMissions() { + public void refreshMissionItems() { for (ViewHolderItem h : mPendingDownloadsItems) { if (((DownloadMission) h.item.mission).running) continue; updateProgress(h); - h.lastTimeStamp = -1; - h.lastDone = -1; + h.resetSpeedMeasure(); } } - public void deleterDispose(boolean commitChanges) { - if (mDeleter != null) mDeleter.dispose(commitChanges); + public void onDestroy() { + mDeleter.dispose(); } - public void deleterLoad(View view) { - if (mDeleter == null) - mDeleter = new Deleter(view, mContext, this, mDownloadManager, mIterator, mHandler); + public void onResume() { + mDeleter.resume(); + mHandler.post(rUpdater); } - public void deleterResume() { - if (mDeleter != null) mDeleter.resume(); - } - - public void recoverMission(DownloadMission mission) { - for (ViewHolderItem h : mPendingDownloadsItems) { - if (mission != h.item.mission) continue; - - mission.errObject = null; - mission.resetState(true, false, DownloadMission.ERROR_NOTHING); - - h.status.setText(UNDEFINED_PROGRESS); - h.state = -1; - h.size.setText(Utility.formatBytes(mission.getLength())); - h.progress.setMarquee(true); - - mDownloadManager.resumeMission(mission); - return; - } - - } - - - private boolean mUpdaterRunning = false; - private final Runnable rUpdater = this::updater; - public void onPaused() { - setAutoRefresh(false); + mDeleter.pause(); + mHandler.removeCallbacks(rUpdater); } - private void setAutoRefresh(boolean enabled) { - if (enabled && !mUpdaterRunning) { - mUpdaterRunning = true; - updater(); - } else if (!enabled && mUpdaterRunning) { - mUpdaterRunning = false; - mHandler.removeCallbacks(rUpdater); - } + + public void recoverMission(DownloadMission mission) { + ViewHolderItem h = getViewHolder(mission); + if (h == null) return; + + mission.errObject = null; + mission.resetState(true, false, DownloadMission.ERROR_NOTHING); + + h.status.setText(UNDEFINED_PROGRESS); + h.size.setText(Utility.formatBytes(mission.getLength())); + h.progress.setMarquee(true); + + mDownloadManager.resumeMission(mission); } private void updater() { - if (!mUpdaterRunning) return; - - boolean running = false; for (ViewHolderItem h : mPendingDownloadsItems) { // check if the mission is running first if (!((DownloadMission) h.item.mission).running) continue; updateProgress(h); - running = true; } - if (running) { - mHandler.postDelayed(rUpdater, 1000); - } else { - mUpdaterRunning = false; - } + mHandler.postDelayed(rUpdater, 1000); } - private boolean isNotFinite(Float value) { - return Float.isNaN(value) || Float.isInfinite(value); + private boolean isNotFinite(double value) { + return Double.isNaN(value) || Double.isInfinite(value); } public void setRecover(@NonNull RecoverHelper callback) { @@ -789,10 +768,11 @@ public class MissionAdapter extends Adapter implements Handler.Callb MenuItem source; MenuItem checksum; - long lastTimeStamp = -1; - long lastDone = -1; - int lastCurrent = -1; - int state = 0; + long lastTimestamp = -1; + double lastDone; + int lastSpeedIdx; + float[] lastSpeed = new float[3]; + String estimatedTimeArrival = UNDEFINED_ETA; ViewHolderItem(View view) { super(view); @@ -902,6 +882,12 @@ public class MissionAdapter extends Adapter implements Handler.Callb return popup; } + + private void resetSpeedMeasure() { + estimatedTimeArrival = UNDEFINED_ETA; + lastTimestamp = -1; + lastSpeedIdx = -1; + } } class ViewHolderHeader extends RecyclerView.ViewHolder { diff --git a/app/src/main/java/us/shandian/giga/ui/common/Deleter.java b/app/src/main/java/us/shandian/giga/ui/common/Deleter.java index 81b4e33e8..a0828c23d 100644 --- a/app/src/main/java/us/shandian/giga/ui/common/Deleter.java +++ b/app/src/main/java/us/shandian/giga/ui/common/Deleter.java @@ -4,9 +4,10 @@ import android.content.Context; import android.content.Intent; import android.graphics.Color; import android.os.Handler; -import com.google.android.material.snackbar.Snackbar; import android.view.View; +import com.google.android.material.snackbar.Snackbar; + import org.schabi.newpipe.R; import java.util.ArrayList; @@ -113,7 +114,7 @@ public class Deleter { show(); } - private void pause() { + public void pause() { running = false; mHandler.removeCallbacks(rNext); mHandler.removeCallbacks(rShow); @@ -126,13 +127,11 @@ public class Deleter { mHandler.postDelayed(rShow, DELAY_RESUME); } - public void dispose(boolean commitChanges) { + public void dispose() { if (items.size() < 1) return; pause(); - if (!commitChanges) return; - for (Mission mission : items) mDownloadManager.deleteMission(mission); items = null; } diff --git a/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java b/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java index a0ff24aaa..3f638d418 100644 --- a/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java +++ b/app/src/main/java/us/shandian/giga/ui/common/ProgressDrawable.java @@ -9,6 +9,7 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Looper; + import androidx.annotation.ColorInt; import androidx.annotation.NonNull; @@ -35,8 +36,8 @@ public class ProgressDrawable extends Drawable { mForegroundColor = foreground; } - public void setProgress(float progress) { - mProgress = progress; + public void setProgress(double progress) { + mProgress = (float) progress; invalidateSelf(); } diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index 26da47b1f..921eaff5c 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -12,11 +12,6 @@ import android.os.Bundle; import android.os.Environment; import android.os.IBinder; import android.preference.PreferenceManager; -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.GridLayoutManager; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -24,6 +19,12 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + import com.nononsenseapps.filepicker.Utils; import org.schabi.newpipe.R; @@ -72,8 +73,7 @@ public class MissionsFragment extends Fragment { mBinder = (DownloadManagerBinder) binder; mBinder.clearDownloadNotifications(); - mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty); - mAdapter.deleterLoad(getView()); + mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty, getView()); mAdapter.setRecover(MissionsFragment.this::recoverMission); @@ -132,7 +132,7 @@ public class MissionsFragment extends Fragment { * Added in API level 23. */ @Override - public void onAttach(Context context) { + public void onAttach(@NonNull Context context) { super.onAttach(context); // Bug: in api< 23 this is never called @@ -147,7 +147,7 @@ public class MissionsFragment extends Fragment { */ @SuppressWarnings("deprecation") @Override - public void onAttach(Activity activity) { + public void onAttach(@NonNull Activity activity) { super.onAttach(activity); mContext = activity; @@ -162,7 +162,7 @@ public class MissionsFragment extends Fragment { mBinder.removeMissionEventListener(mAdapter); mBinder.enableNotifications(true); mContext.unbindService(mConnection); - mAdapter.deleterDispose(true); + mAdapter.onDestroy(); mBinder = null; mAdapter = null; @@ -196,13 +196,11 @@ public class MissionsFragment extends Fragment { prompt.create().show(); return true; case R.id.start_downloads: - item.setVisible(false); mBinder.getDownloadManager().startAllMissions(); return true; case R.id.pause_downloads: - item.setVisible(false); mBinder.getDownloadManager().pauseAllMissions(false); - mAdapter.ensurePausedMissions();// update items view + mAdapter.refreshMissionItems();// update items view default: return super.onOptionsItemSelected(item); } @@ -271,23 +269,12 @@ public class MissionsFragment extends Fragment { } } - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - - if (mAdapter != null) { - mAdapter.deleterDispose(false); - mForceUpdate = true; - mBinder.removeMissionEventListener(mAdapter); - } - } - @Override public void onResume() { super.onResume(); if (mAdapter != null) { - mAdapter.deleterResume(); + mAdapter.onResume(); if (mForceUpdate) { mForceUpdate = false; @@ -303,7 +290,13 @@ public class MissionsFragment extends Fragment { @Override public void onPause() { super.onPause(); - if (mAdapter != null) mAdapter.onPaused(); + + if (mAdapter != null) { + mForceUpdate = true; + mBinder.removeMissionEventListener(mAdapter); + mAdapter.onPaused(); + } + if (mBinder != null) mBinder.enableNotifications(true); } diff --git a/app/src/main/java/us/shandian/giga/util/Utility.java b/app/src/main/java/us/shandian/giga/util/Utility.java index 21fdd72ad..46207777a 100644 --- a/app/src/main/java/us/shandian/giga/util/Utility.java +++ b/app/src/main/java/us/shandian/giga/util/Utility.java @@ -4,13 +4,14 @@ import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.os.Build; +import android.util.Log; +import android.widget.Toast; + import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; -import android.util.Log; -import android.widget.Toast; import org.schabi.newpipe.R; import org.schabi.newpipe.streams.io.SharpStream; @@ -26,6 +27,7 @@ import java.io.Serializable; import java.net.HttpURLConnection; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.Locale; import us.shandian.giga.io.StoredFileHelper; @@ -39,26 +41,28 @@ public class Utility { } public static String formatBytes(long bytes) { + Locale locale = Locale.getDefault(); if (bytes < 1024) { - return String.format("%d B", bytes); + return String.format(locale, "%d B", bytes); } else if (bytes < 1024 * 1024) { - return String.format("%.2f kB", bytes / 1024d); + return String.format(locale, "%.2f kB", bytes / 1024d); } else if (bytes < 1024 * 1024 * 1024) { - return String.format("%.2f MB", bytes / 1024d / 1024d); + return String.format(locale, "%.2f MB", bytes / 1024d / 1024d); } else { - return String.format("%.2f GB", bytes / 1024d / 1024d / 1024d); + return String.format(locale, "%.2f GB", bytes / 1024d / 1024d / 1024d); } } - public static String formatSpeed(float speed) { + public static String formatSpeed(double speed) { + Locale locale = Locale.getDefault(); if (speed < 1024) { - return String.format("%.2f B/s", speed); + return String.format(locale, "%.2f B/s", speed); } else if (speed < 1024 * 1024) { - return String.format("%.2f kB/s", speed / 1024); + return String.format(locale, "%.2f kB/s", speed / 1024); } else if (speed < 1024 * 1024 * 1024) { - return String.format("%.2f MB/s", speed / 1024 / 1024); + return String.format(locale, "%.2f MB/s", speed / 1024 / 1024); } else { - return String.format("%.2f GB/s", speed / 1024 / 1024 / 1024); + return String.format(locale, "%.2f GB/s", speed / 1024 / 1024 / 1024); } } @@ -188,12 +192,11 @@ public class Utility { switch (type) { case MUSIC: return R.drawable.music; + default: case VIDEO: return R.drawable.video; case SUBTITLE: return R.drawable.subtitle; - default: - return R.drawable.video; } } @@ -274,4 +277,25 @@ public class Utility { return -1; } + + private static String pad(int number) { + return number < 10 ? ("0" + number) : String.valueOf(number); + } + + public static String stringifySeconds(double seconds) { + int h = (int) Math.floor(seconds / 3600); + int m = (int) Math.floor((seconds - (h * 3600)) / 60); + int s = (int) (seconds - (h * 3600) - (m * 60)); + + String str = ""; + + if (h < 1 && m < 1) { + str = "00:"; + } else { + if (h > 0) str = pad(h) + ":"; + if (m > 0) str += pad(m) + ":"; + } + + return str + pad(s); + } } diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 43b45d15e..86cbbb59a 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -471,7 +471,6 @@ غير موجود فشلت المعالجة الاولية حذف التنزيلات المنتهية - "قم بإستكمال %s حيثما يتم التحويل من التنزيلات" توقف أقصى عدد للمحاولات الحد الأقصى لعدد محاولات قبل إلغاء التحميل diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 3c79a96d3..1cf3abd7e 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -458,7 +458,6 @@ Не знойдзена Пасляапрацоўка не ўдалася Ачысціць завершаныя - Аднавіць прыпыненыя загрузкі (%s) Спыніць Максімум спробаў Колькасць спробаў перад адменай загрузкі diff --git a/app/src/main/res/values-cmn/strings.xml b/app/src/main/res/values-cmn/strings.xml index bcb145c16..3ff479bfd 100644 --- a/app/src/main/res/values-cmn/strings.xml +++ b/app/src/main/res/values-cmn/strings.xml @@ -460,7 +460,6 @@ NewPipe 更新可用! 无法创建目标文件夹 服务器不接受多线程下载, 请使用 @string/msg_threads = 1重试 - 继续进行%s个待下载转移 切换至移动数据时有用,尽管一些下载无法被暂停 显示评论 禁用停止显示评论 diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index b741e0d16..9a9cc8654 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -466,7 +466,6 @@ otevření ve vyskakovacím okně Nenalezeno Post-processing selhal Vyčistit dokončená stahování - Pokračovat ve stahování %s souborů, čekajících na stažení Zastavit Maximální počet pokusů o opakování Maximální počet pokusů před zrušením stahování diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 199c2f85d..5e44aab61 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -447,7 +447,6 @@ sat på pause sat i kø Ryd færdige downloads - Fortsæt dine %s ventende overførsler fra Downloads Maksimalt antal genforsøg Maksimalt antal forsøg før downloaden opgives Sæt på pause ved skift til mobildata diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 3279e919c..0dc0de8b4 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -457,7 +457,6 @@ Nicht gefunden Nachbearbeitung fehlgeschlagen Um fertige Downloads bereinigen - Setze deine %s ausstehenden Übertragungen von Downloads fort Anhalten Maximale Wiederholungen Maximalanzahl der Versuche, bevor der Download abgebrochen wird diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 372cbb1a2..115b8d0b3 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -459,7 +459,6 @@ Δεν βρέθηκε Μετεπεξεργασία απέτυχε Εκκαθάριση ολοκληρωμένων λήψεων - Συνέχιση των %s εκκρεμών σας λήψεων Διακοπή Μέγιστες επαναπροσπάθειες Μέγιστος αριθμός προσπαθειών προτού γίνει ακύρωση της λήψης diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index b14aab94b..6fcbc9fa7 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -406,6 +406,7 @@ pausado en cola posprocesamiento + recuperando Añadir a cola Acción denegada por el sistema Se eliminó el archivo @@ -424,7 +425,6 @@ Mostrar como grilla Mostrar como lista Limpiar descargas finalizadas - Tienes %s descargas pendientes, ve a Descargas para continuarlas ¿Lo confirma\? Detener Intentos máximos diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 4dfcc3d0e..99dc6cc80 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -460,7 +460,6 @@ Ei leitud Järeltöötlemine nurjus Eemalda lõpetatud allalaadimised - Jätka %s pooleliolevat allalaadimist Stopp Korduskatseid Suurim katsete arv enne allalaadimise tühistamist diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 7b636d383..743c6b3fb 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -459,7 +459,6 @@ Ez aurkitua Post-prozesuak huts egin du Garbitu amaitutako deskargak - Berrekin burutzeke dauden %s transferentzia deskargetatik Gelditu Gehienezko saiakerak Deskarga ezeztatu aurretik saiatu beharreko aldi kopurua diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index b4388e39f..2091a62fe 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -466,7 +466,6 @@ Nombre maximum de tentatives avant d’annuler le téléchargement Utilisation des onglets par défaut, erreur lors de la lecture des onglets enregistrés Le serveur n’accepte pas les téléchargements multi-fils, veuillez réessayer avec @string/msg_threads = 1 - Continuer vos %s transferts en attente depuis Téléchargement Afficher les commentaires Désactiver pour ne pas afficher les commentaires Lecture automatique diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 5e340d8b3..565f815a1 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -464,7 +464,6 @@ לא נמצא העיבוד המאוחר נכשל פינוי ההורדות שהסתיימו - ניתן להמשיך את %s ההורדות הממתינות שלך דרך ההורדות עצירה מספר הניסיונות החוזרים המרבי מספר הניסיונות החוזרים המרבי בטרם ביטול ההורדה diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index aa4ff9113..a981dcf5e 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -457,7 +457,6 @@ Nije pronađeno Naknadna obrada nije uspjela Obriši završena preuzimanja - Nastavite s prijenosima na čekanju za %s s preuzimanja Stop Maksimalnih ponovnih pokušaja Maksimalni broj pokušaja prije poništavanja preuzimanja diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index d52f5fafa..5fbdcffc1 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -453,7 +453,6 @@ Tidak ditemukan Pengolahan-pasca gagal Hapus unduhan yang sudah selesai - Lanjutkan %s transfer anda yang tertunda dari Unduhan Berhenti Percobaan maksimum Jumlah upaya maksimum sebelum membatalkan unduhan diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index c92292f99..73633ab03 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -457,7 +457,6 @@ Non trovato Post-processing fallito Pulisci i download completati - Continua i %s trasferimenti in corso dai Download Ferma Tentativi massimi Tentativi massimi prima di cancellare il download diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 58ca2ebff..4c3aeb5c1 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -456,7 +456,6 @@ デフォルトのタブを使用します。保存されたタブの読み込みエラーが発生しました メインページに表示されるタブ 新しいバージョンが利用可能なときにアプリの更新を確認する通知を表示します - ダウンロードから %s の保留中の転送を続行します 従量制課金ネットワークの割り込み モバイルデータ通信に切り替える場合に便利ですが、一部のダウンロードは一時停止できません コメントを表示 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index fdc76b04e..39b08347c 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -454,7 +454,6 @@ HTTP 찾을 수 없습니다 후처리 작업이 실패하였습니다 완료된 다운로드 비우기 - 대기중인 %s 다운로드를 지속하세요 멈추기 최대 재시도 횟수 다운로드를 취소하기 전까지 다시 시도할 최대 횟수 diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index daa120ea2..354e7b7de 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -453,7 +453,6 @@ Tidak ditemui Pemprosesan-pasca gagal Hapuskan senarai muat turun yang selesai - Teruskan %s pemindahan anda yang menunggu dari muat turun Berhenti Percubaan maksimum Jumlah percubaan maksimum sebelum membatalkan muat turun diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 6262480b0..e0a08d0a7 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -458,7 +458,6 @@ Ikke funnet Etterbehandling mislyktes Tøm fullførte nedlastinger - Fortsett dine %s ventende overføringer fra Nedlastinger Stopp Maksimalt antall forsøk Maksimalt antall tilkoblingsforsøk før nedlastingen avblåses diff --git a/app/src/main/res/values-nl-rBE/strings.xml b/app/src/main/res/values-nl-rBE/strings.xml index f64ff6bf9..5c42bfd23 100644 --- a/app/src/main/res/values-nl-rBE/strings.xml +++ b/app/src/main/res/values-nl-rBE/strings.xml @@ -457,7 +457,6 @@ Niet gevonden Nabewerking mislukt Voltooide downloads wissen - Zet uw %s wachtende downloads verder via Downloads Stoppen Maximaal aantal pogingen Maximaal aantal pogingen vooraleer dat den download wordt geannuleerd diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 6aecc2cd1..b9b86a292 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -457,7 +457,6 @@ Niet gevonden Nabewerking mislukt Voltooide downloads wissen - Zet je %s wachtende downloads voort via Downloads Stop Maximum aantal keer proberen Maximum aantal pogingen voordat de download wordt geannuleerd diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index b57564eba..0e579720a 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -453,7 +453,6 @@ ਨਹੀਂ ਲਭਿਆ Post-processing ਫੇਲ੍ਹ ਮੁਕੰਮਲ ਹੋਈਆਂ ਡਾਊਨਲੋਡ ਸਾਫ਼ ਕਰੋ - ਡਾਉਨਲੋਡਸ ਤੋਂ ਆਪਣੀਆਂ %s ਬਕਾਇਆ ਟ੍ਰਾਂਸਫਰ ਜਾਰੀ ਰੱਖੋ ਰੁੱਕੋ ਵੱਧ ਤੋਂ ਵੱਧ ਕੋਸ਼ਿਸ਼ਾਂ ਡਾਉਨਲੋਡ ਰੱਦ ਕਰਨ ਤੋਂ ਪਹਿਲਾਂ ਵੱਧ ਤੋਂ ਵੱਧ ਕੋਸ਼ਿਸ਼ਾਂ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index ca1e52ff2..b7086b34f 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -459,7 +459,6 @@ Nie znaleziono Przetwarzanie końcowe nie powiodło się Wyczyść ukończone pobieranie - Kontynuuj %s oczekujące transfery z plików do pobrania Zatrzymaj Maksymalna liczba powtórzeń Maksymalna liczba prób przed anulowaniem pobierania diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 0bdf4d006..5de1e6610 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -466,7 +466,6 @@ abrir em modo popup Não encontrado Falha no pós processamento Limpar downloads finalizados - Continuar seus %s downloads pendentes Parar Tentativas Máximas Número máximo de tentativas antes de cancelar o download diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 6d55023d1..88fbb72a6 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -455,7 +455,6 @@ Não encontrado Pós-processamento falhado Limpar transferências concluídas - Continue as suas %s transferências pendentes das Transferências Parar Tentativas máximas Número máximo de tentativas antes de cancelar a transferência diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 51771e1b1..80b587657 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -464,7 +464,6 @@ Загрузка завершена %s загрузок завершено Создать уникальное имя - Возобновить приостановленные загрузки (%s) Максимум попыток Количество попыток перед отменой загрузки Некоторые загрузки не поддерживают докачку и начнутся с начала diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 36c0afd84..cbc201fd5 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -465,7 +465,6 @@ Nenájdené Post-spracovanie zlyhalo Vyčistiť dokončené sťahovania - Pokračujte v preberaní %s zo súborov na prevzatie Stop Maximum opakovaní Maximálny počet pokusov pred zrušením stiahnutia diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 6c9c66f69..1cb6fafd4 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -452,7 +452,6 @@ Bulunamadı İşlem sonrası başarısız Tamamlanan indirmeleri temizle - Beklemedeki %s transferinize İndirmeler\'den devam edin Durdur Azami deneme sayısı İndirmeyi iptal etmeden önce maksimum deneme sayısı diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index fcce99e89..d43b8be66 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -471,7 +471,6 @@ Помилка зчитування збережених вкладок. Використовую типові вкладки. Вкладки, що відображаються на головній сторінці Показувати сповіщення з пропозицією оновити застосунок за наявності нової версії - Продовжити ваші %s відкладених переміщень із Завантажень Корисно під час переходу на мобільні дані, хоча деякі завантаження не можуть бути призупинені Показувати коментарі Вимнути відображення дописів diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index f8860acfd..ab0983e7a 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -452,7 +452,6 @@ Không tìm thấy Xử lý thất bại Dọn các tải về đã hoàn thành - Hãy tiếp tục %s tải về đang chờ Dừng Số lượt thử lại tối đa Số lượt thử lại trước khi hủy tải về diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 310bae3a3..98b9cf381 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -450,7 +450,6 @@ 找不到 後處理失敗 清除已結束的下載 - 繼續從您所擱置中的下載 %s 傳輸 停止 最大重試次數 在取消下載前的最大嘗試數 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f929e0d2b..c2d8d70f2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -526,6 +526,7 @@ paused queued post-processing + recovering Queue Action denied by the system @@ -560,7 +561,6 @@ Cannot recover this download Clear finished downloads Are you sure? - Continue your %s pending transfers from Downloads Stop Maximum retries Maximum number of attempts before canceling the download