mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-30 23:03:00 +00:00 
			
		
		
		
	main commit
Post-processing infrastructure * remove interfaces with one implementation * fix download resources with unknow length * marquee style for ProgressDrawable * "view details" option in mission context menu * notification for finished downloads * postprocessing infrastructure: sub-missions, circular file, layers for layers of abstractions for Java IO streams * Mp4 muxing (only DASH brand) * WebM muxing * Captions downloading * alert dialog for overwrite existing downloads finished or not. Misc changes * delete SQLiteDownloadDataSource.java * delete DownloadMissionSQLiteHelper.java * implement Localization from #114 Misc fixes (this branch) * restore old mission listeners variables. Prevents registered listeners get de-referenced on low-end devices * DownloadManagerService.checkForRunningMission() now return false if the mission has a error. * use Intent.FLAG_ACTIVITY_NEW_TASK when launching an activity from gigaget threads (apparently it is required in old versions of android) More changes * proper error handling "infrastructure" * queue instead of multiple downloads * move serialized pending downloads (.giga files) to app data * stop downloads when swicthing to mobile network (never works, see 2nd point) * save the thread count for next downloads * a lot of incoherences fixed * delete DownloadManagerTest.java (too many changes to keep this file updated)
This commit is contained in:
		| @@ -89,7 +89,8 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader { | ||||
|                     .build(); | ||||
|             response = client.newCall(request).execute(); | ||||
|  | ||||
|             return Long.parseLong(response.header("Content-Length")); | ||||
|             String contentLength = response.header("Content-Length"); | ||||
|             return contentLength == null ? -1 : Long.parseLong(contentLength); | ||||
|         } catch (NumberFormatException e) { | ||||
|             throw new IOException("Invalid content length", e); | ||||
|         } finally { | ||||
| @@ -104,13 +105,13 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader { | ||||
|      * but set the HTTP header field "Accept-Language" to the supplied string. | ||||
|      * | ||||
|      * @param siteUrl  the URL of the text file to return the contents of | ||||
|      * @param localization the language and country (usually a 2-character code) to set | ||||
|      * @param localisation the language and country (usually a 2-character code) to set | ||||
|      * @return the contents of the specified text file | ||||
|      */ | ||||
|     @Override | ||||
|     public String download(String siteUrl, Localization localization) throws IOException, ReCaptchaException { | ||||
|     public String download(String siteUrl, Localization localisation) throws IOException, ReCaptchaException { | ||||
|         Map<String, String> requestProperties = new HashMap<>(); | ||||
|         requestProperties.put("Accept-Language", localization.getLanguage()); | ||||
|         requestProperties.put("Accept-Language", localisation.getLanguage()); | ||||
|         return download(siteUrl, requestProperties); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -28,14 +28,14 @@ public class DeleteDownloadManager { | ||||
|  | ||||
|     private static final String KEY_STATE = "delete_manager_state"; | ||||
|  | ||||
|     private final View mView; | ||||
|     private final HashSet<String> mPendingMap; | ||||
|     private final List<Disposable> mDisposableList; | ||||
|     private View mView; | ||||
|     private ArrayList<Long> mPendingMap; | ||||
|     private List<Disposable> mDisposableList; | ||||
|     private DownloadManager mDownloadManager; | ||||
|     private final PublishSubject<DownloadMission> publishSubject = PublishSubject.create(); | ||||
|  | ||||
|     DeleteDownloadManager(Activity activity) { | ||||
|         mPendingMap = new HashSet<>(); | ||||
|         mPendingMap = new ArrayList<>(); | ||||
|         mDisposableList = new ArrayList<>(); | ||||
|         mView = activity.findViewById(android.R.id.content); | ||||
|     } | ||||
| @@ -45,11 +45,11 @@ public class DeleteDownloadManager { | ||||
|     } | ||||
|  | ||||
|     public boolean contains(@NonNull DownloadMission mission) { | ||||
|         return mPendingMap.contains(mission.url); | ||||
|         return mPendingMap.contains(mission.timestamp); | ||||
|     } | ||||
|  | ||||
|     public void add(@NonNull DownloadMission mission) { | ||||
|         mPendingMap.add(mission.url); | ||||
|         mPendingMap.add(mission.timestamp); | ||||
|  | ||||
|         if (mPendingMap.size() == 1) { | ||||
|             showUndoDeleteSnackbar(mission); | ||||
| @@ -67,9 +67,10 @@ public class DeleteDownloadManager { | ||||
|     public void restoreState(@Nullable Bundle savedInstanceState) { | ||||
|         if (savedInstanceState == null) return; | ||||
|  | ||||
|         List<String> list = savedInstanceState.getStringArrayList(KEY_STATE); | ||||
|         long[] list = savedInstanceState.getLongArray(KEY_STATE); | ||||
|         if (list != null) { | ||||
|             mPendingMap.addAll(list); | ||||
|             mPendingMap.ensureCapacity(mPendingMap.size() + list.length); | ||||
|             for (long timestamp : list) mPendingMap.add(timestamp); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -80,17 +81,20 @@ public class DeleteDownloadManager { | ||||
|             disposable.dispose(); | ||||
|         } | ||||
|  | ||||
|         outState.putStringArrayList(KEY_STATE, new ArrayList<>(mPendingMap)); | ||||
|         long[] list = new long[mPendingMap.size()]; | ||||
|         for (int i = 0; i < mPendingMap.size(); i++) list[i] = mPendingMap.get(i); | ||||
|  | ||||
|         outState.putLongArray(KEY_STATE, list); | ||||
|     } | ||||
|  | ||||
|     private void showUndoDeleteSnackbar() { | ||||
|         if (mPendingMap.size() < 1) return; | ||||
|  | ||||
|         String url = mPendingMap.iterator().next(); | ||||
|         long timestamp = mPendingMap.iterator().next(); | ||||
|  | ||||
|         for (int i = 0; i < mDownloadManager.getCount(); i++) { | ||||
|             DownloadMission mission = mDownloadManager.getMission(i); | ||||
|             if (url.equals(mission.url)) { | ||||
|             if (timestamp == mission.timestamp) { | ||||
|                 showUndoDeleteSnackbar(mission); | ||||
|                 break; | ||||
|             } | ||||
| @@ -106,7 +110,7 @@ public class DeleteDownloadManager { | ||||
|         mDisposableList.add(disposable); | ||||
|  | ||||
|         snackbar.setAction(R.string.undo, v -> { | ||||
|             mPendingMap.remove(mission.url); | ||||
|             mPendingMap.remove(mission.timestamp); | ||||
|             publishSubject.onNext(mission); | ||||
|             disposable.dispose(); | ||||
|             snackbar.dismiss(); | ||||
| @@ -115,12 +119,13 @@ public class DeleteDownloadManager { | ||||
|         snackbar.addCallback(new BaseTransientBottomBar.BaseCallback<Snackbar>() { | ||||
|             @Override | ||||
|             public void onDismissed(Snackbar transientBottomBar, int event) { | ||||
|                 // TODO: disposable.isDisposed() is always true. fix this | ||||
|                 if (!disposable.isDisposed()) { | ||||
|                     Completable.fromAction(() -> deletePending(mission)) | ||||
|                             .subscribeOn(Schedulers.io()) | ||||
|                             .subscribe(); | ||||
|                 } | ||||
|                 mPendingMap.remove(mission.url); | ||||
|                 mPendingMap.remove(mission.timestamp); | ||||
|                 snackbar.removeCallback(this); | ||||
|                 mDisposableList.remove(disposable); | ||||
|                 showUndoDeleteSnackbar(); | ||||
| @@ -149,7 +154,7 @@ public class DeleteDownloadManager { | ||||
|  | ||||
|     private void deletePending(@NonNull DownloadMission mission) { | ||||
|         for (int i = 0; i < mDownloadManager.getCount(); i++) { | ||||
|             if (mission.url.equals(mDownloadManager.getMission(i).url)) { | ||||
|             if (mission.timestamp == mDownloadManager.getMission(i).timestamp) { | ||||
|                 mDownloadManager.deleteMission(i); | ||||
|                 break; | ||||
|             } | ||||
|   | ||||
| @@ -15,19 +15,16 @@ import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.settings.SettingsActivity; | ||||
| import org.schabi.newpipe.util.ThemeHelper; | ||||
|  | ||||
| import io.reactivex.Completable; | ||||
| import io.reactivex.schedulers.Schedulers; | ||||
| import us.shandian.giga.service.DownloadManagerService; | ||||
| import us.shandian.giga.ui.fragment.AllMissionsFragment; | ||||
| import us.shandian.giga.ui.fragment.MissionsFragment; | ||||
|  | ||||
| public class DownloadActivity extends AppCompatActivity { | ||||
|  | ||||
|     private static final String MISSIONS_FRAGMENT_TAG = "fragment_tag"; | ||||
|     private DeleteDownloadManager mDeleteDownloadManager; | ||||
|  | ||||
|     @Override | ||||
|     protected void onCreate(Bundle savedInstanceState) { | ||||
|  | ||||
|         // Service | ||||
|         Intent i = new Intent(); | ||||
|         i.setClass(this, DownloadManagerService.class); | ||||
| @@ -47,13 +44,6 @@ public class DownloadActivity extends AppCompatActivity { | ||||
|             actionBar.setDisplayShowTitleEnabled(true); | ||||
|         } | ||||
|  | ||||
|         mDeleteDownloadManager = new DeleteDownloadManager(this); | ||||
|         mDeleteDownloadManager.restoreState(savedInstanceState); | ||||
|  | ||||
|         MissionsFragment fragment = (MissionsFragment) getFragmentManager().findFragmentByTag(MISSIONS_FRAGMENT_TAG); | ||||
|         if (fragment != null) { | ||||
|             fragment.setDeleteManager(mDeleteDownloadManager); | ||||
|         } else { | ||||
|         getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { | ||||
|             @Override | ||||
|             public void onGlobalLayout() { | ||||
| @@ -62,17 +52,9 @@ public class DownloadActivity extends AppCompatActivity { | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void onSaveInstanceState(Bundle outState) { | ||||
|         mDeleteDownloadManager.saveState(outState); | ||||
|         super.onSaveInstanceState(outState); | ||||
|     } | ||||
|  | ||||
|     private void updateFragments() { | ||||
|         MissionsFragment fragment = new AllMissionsFragment(); | ||||
|         fragment.setDeleteManager(mDeleteDownloadManager); | ||||
|         MissionsFragment fragment = new MissionsFragment(); | ||||
|  | ||||
|         getFragmentManager().beginTransaction() | ||||
|                 .replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG) | ||||
| @@ -99,7 +81,6 @@ public class DownloadActivity extends AppCompatActivity { | ||||
|             case R.id.action_settings: { | ||||
|                 Intent intent = new Intent(this, SettingsActivity.class); | ||||
|                 startActivity(intent); | ||||
|                 deletePending(); | ||||
|                 return true; | ||||
|             } | ||||
|             default: | ||||
| @@ -108,14 +89,7 @@ public class DownloadActivity extends AppCompatActivity { | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onBackPressed() { | ||||
|         super.onBackPressed(); | ||||
|         deletePending(); | ||||
|     } | ||||
|  | ||||
|     private void deletePending() { | ||||
|         Completable.fromAction(mDeleteDownloadManager::deletePending) | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .subscribe(); | ||||
|     public void onRestoreInstanceState(Bundle inState){ | ||||
|         super.onRestoreInstanceState(inState); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,14 @@ | ||||
| package org.schabi.newpipe.download; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.content.SharedPreferences; | ||||
| import android.os.Bundle; | ||||
| import android.preference.PreferenceManager; | ||||
| import android.support.annotation.IdRes; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.v4.app.DialogFragment; | ||||
| import android.support.v7.app.AlertDialog; | ||||
| import android.support.v7.widget.Toolbar; | ||||
| import android.util.Log; | ||||
| import android.view.LayoutInflater; | ||||
| @@ -22,10 +25,14 @@ import android.widget.Toast; | ||||
|  | ||||
| import org.schabi.newpipe.MainActivity; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.extractor.MediaFormat; | ||||
| import org.schabi.newpipe.extractor.NewPipe; | ||||
| import org.schabi.newpipe.extractor.stream.AudioStream; | ||||
| import org.schabi.newpipe.extractor.stream.Stream; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.extractor.stream.SubtitlesStream; | ||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | ||||
| import org.schabi.newpipe.extractor.utils.Localization; | ||||
| import org.schabi.newpipe.settings.NewPipeSettings; | ||||
| import org.schabi.newpipe.util.FilenameUtils; | ||||
| import org.schabi.newpipe.util.ListHelper; | ||||
| @@ -36,24 +43,36 @@ import org.schabi.newpipe.util.ThemeHelper; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
|  | ||||
| import icepick.Icepick; | ||||
| import icepick.State; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
| import us.shandian.giga.postprocessing.Postprocessing; | ||||
| import us.shandian.giga.service.DownloadManagerService; | ||||
|  | ||||
| public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener { | ||||
|     private static final String TAG = "DialogFragment"; | ||||
|     private static final boolean DEBUG = MainActivity.DEBUG; | ||||
|  | ||||
|     @State protected StreamInfo currentInfo; | ||||
|     @State protected StreamSizeWrapper<AudioStream> wrappedAudioStreams = StreamSizeWrapper.empty(); | ||||
|     @State protected StreamSizeWrapper<VideoStream> wrappedVideoStreams = StreamSizeWrapper.empty(); | ||||
|     @State protected int selectedVideoIndex = 0; | ||||
|     @State protected int selectedAudioIndex = 0; | ||||
|     @State | ||||
|     protected StreamInfo currentInfo; | ||||
|     @State | ||||
|     protected StreamSizeWrapper<AudioStream> wrappedAudioStreams = StreamSizeWrapper.empty(); | ||||
|     @State | ||||
|     protected StreamSizeWrapper<VideoStream> wrappedVideoStreams = StreamSizeWrapper.empty(); | ||||
|     @State | ||||
|     protected StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams = StreamSizeWrapper.empty(); | ||||
|     @State | ||||
|     protected int selectedVideoIndex = 0; | ||||
|     @State | ||||
|     protected int selectedAudioIndex = 0; | ||||
|     @State | ||||
|     protected int selectedSubtitleIndex = 0; | ||||
|  | ||||
|     private StreamItemAdapter<AudioStream> audioStreamsAdapter; | ||||
|     private StreamItemAdapter<VideoStream> videoStreamsAdapter; | ||||
|     private StreamItemAdapter<SubtitlesStream> subtitleStreamsAdapter; | ||||
|  | ||||
|     private final CompositeDisposable disposables = new CompositeDisposable(); | ||||
|  | ||||
| @@ -63,6 +82,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|     private TextView threadsCountTextView; | ||||
|     private SeekBar threadsSeekBar; | ||||
|  | ||||
|     private SharedPreferences prefs; | ||||
|  | ||||
|     public static DownloadDialog newInstance(StreamInfo info) { | ||||
|         DownloadDialog dialog = new DownloadDialog(); | ||||
|         dialog.setInfo(info); | ||||
| @@ -78,6 +99,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|         instance.setVideoStreams(streamsList); | ||||
|         instance.setSelectedVideoStream(selectedStreamIndex); | ||||
|         instance.setAudioStreams(info.getAudioStreams()); | ||||
|         instance.setSubtitleStreams(info.getSubtitles()); | ||||
|  | ||||
|         return instance; | ||||
|     } | ||||
|  | ||||
| @@ -86,7 +109,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|     } | ||||
|  | ||||
|     public void setAudioStreams(List<AudioStream> audioStreams) { | ||||
|         setAudioStreams(new StreamSizeWrapper<>(audioStreams)); | ||||
|         setAudioStreams(new StreamSizeWrapper<>(audioStreams, getContext())); | ||||
|     } | ||||
|  | ||||
|     public void setAudioStreams(StreamSizeWrapper<AudioStream> wrappedAudioStreams) { | ||||
| @@ -94,13 +117,21 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|     } | ||||
|  | ||||
|     public void setVideoStreams(List<VideoStream> videoStreams) { | ||||
|         setVideoStreams(new StreamSizeWrapper<>(videoStreams)); | ||||
|         setVideoStreams(new StreamSizeWrapper<>(videoStreams, getContext())); | ||||
|     } | ||||
|  | ||||
|     public void setVideoStreams(StreamSizeWrapper<VideoStream> wrappedVideoStreams) { | ||||
|         this.wrappedVideoStreams = wrappedVideoStreams; | ||||
|     } | ||||
|  | ||||
|     public void setSubtitleStreams(List<SubtitlesStream> subtitleStreams) { | ||||
|         setSubtitleStreams(new StreamSizeWrapper<>(subtitleStreams, getContext())); | ||||
|     } | ||||
|  | ||||
|     public void setSubtitleStreams(StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams) { | ||||
|         this.wrappedSubtitleStreams = wrappedSubtitleStreams; | ||||
|     } | ||||
|  | ||||
|     public void setSelectedVideoStream(int selectedVideoIndex) { | ||||
|         this.selectedVideoIndex = selectedVideoIndex; | ||||
|     } | ||||
| @@ -109,6 +140,10 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|         this.selectedAudioIndex = selectedAudioIndex; | ||||
|     } | ||||
|  | ||||
|     public void setSelectedSubtitleStream(int selectedSubtitleIndex) { | ||||
|         this.selectedSubtitleIndex = selectedSubtitleIndex; | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // LifeCycle | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
| @@ -116,7 +151,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|     @Override | ||||
|     public void onCreate(@Nullable Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); | ||||
|         if (DEBUG) | ||||
|             Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); | ||||
|         if (!PermissionHelper.checkStoragePermissions(getActivity(), PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) { | ||||
|             getDialog().dismiss(); | ||||
|             return; | ||||
| @@ -127,11 +163,13 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|  | ||||
|         this.videoStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedVideoStreams, true); | ||||
|         this.audioStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedAudioStreams); | ||||
|         this.subtitleStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedSubtitleStreams); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { | ||||
|         if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]"); | ||||
|         if (DEBUG) | ||||
|             Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]"); | ||||
|         return inflater.inflate(R.layout.download_dialog, container); | ||||
|     } | ||||
|  | ||||
| @@ -142,6 +180,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|         nameEditText.setText(FilenameUtils.createFilename(getContext(), currentInfo.getName())); | ||||
|         selectedAudioIndex = ListHelper.getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams()); | ||||
|  | ||||
|         selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll()); | ||||
|  | ||||
|         streamsSpinner = view.findViewById(R.id.quality_spinner); | ||||
|         streamsSpinner.setOnItemSelectedListener(this); | ||||
|  | ||||
| @@ -154,14 +194,18 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|         initToolbar(view.findViewById(R.id.toolbar)); | ||||
|         setupDownloadOptions(); | ||||
|  | ||||
|         int def = 3; | ||||
|         threadsCountTextView.setText(String.valueOf(def)); | ||||
|         threadsSeekBar.setProgress(def - 1); | ||||
|         prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); | ||||
|  | ||||
|         int threads = prefs.getInt(getString(R.string.default_download_threads), 3); | ||||
|         threadsCountTextView.setText(String.valueOf(threads)); | ||||
|         threadsSeekBar.setProgress(threads - 1); | ||||
|         threadsSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { | ||||
|  | ||||
|             @Override | ||||
|             public void onProgressChanged(SeekBar seekbar, int progress, boolean fromUser) { | ||||
|                 threadsCountTextView.setText(String.valueOf(progress + 1)); | ||||
|                 progress++; | ||||
|                 prefs.edit().putInt(getString(R.string.default_download_threads), progress).apply(); | ||||
|                 threadsCountTextView.setText(String.valueOf(progress)); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
| @@ -189,6 +233,11 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|                 setupAudioSpinner(); | ||||
|             } | ||||
|         })); | ||||
|         disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams).subscribe(result -> { | ||||
|             if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) { | ||||
|                 setupSubtitleSpinner(); | ||||
|             } | ||||
|         })); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -216,7 +265,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|  | ||||
|         toolbar.setOnMenuItemClickListener(item -> { | ||||
|             if (item.getItemId() == R.id.okay) { | ||||
|                 downloadSelected(); | ||||
|                 prepareSelectedDownload(); | ||||
|                 return true; | ||||
|             } | ||||
|             return false; | ||||
| @@ -239,13 +288,24 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|         setRadioButtonsState(true); | ||||
|     } | ||||
|  | ||||
|     private void setupSubtitleSpinner() { | ||||
|         if (getContext() == null) return; | ||||
|  | ||||
|         streamsSpinner.setAdapter(subtitleStreamsAdapter); | ||||
|         streamsSpinner.setSelection(selectedSubtitleIndex); | ||||
|         setRadioButtonsState(true); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
|     // Radio group Video&Audio options - Listener | ||||
|     //////////////////////////////////////////////////////////////////////////*/ | ||||
|  | ||||
|     @Override | ||||
|     public void onCheckedChanged(RadioGroup group, @IdRes int checkedId) { | ||||
|         if (DEBUG) Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]"); | ||||
|         if (DEBUG) | ||||
|             Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]"); | ||||
|         boolean flag = true; | ||||
|  | ||||
|         switch (checkedId) { | ||||
|             case R.id.audio_button: | ||||
|                 setupAudioSpinner(); | ||||
| @@ -253,7 +313,13 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|             case R.id.video_button: | ||||
|                 setupVideoSpinner(); | ||||
|                 break; | ||||
|             case R.id.subtitle_button: | ||||
|                 setupSubtitleSpinner(); | ||||
|                 flag = false; | ||||
|                 break; | ||||
|         } | ||||
|  | ||||
|         threadsSeekBar.setEnabled(flag); | ||||
|     } | ||||
|  | ||||
|     /*////////////////////////////////////////////////////////////////////////// | ||||
| @@ -262,7 +328,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|  | ||||
|     @Override | ||||
|     public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { | ||||
|         if (DEBUG) Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]"); | ||||
|         if (DEBUG) | ||||
|             Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]"); | ||||
|         switch (radioVideoAudioGroup.getCheckedRadioButtonId()) { | ||||
|             case R.id.audio_button: | ||||
|                 selectedAudioIndex = position; | ||||
| @@ -270,6 +337,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|             case R.id.video_button: | ||||
|                 selectedVideoIndex = position; | ||||
|                 break; | ||||
|             case R.id.subtitle_button: | ||||
|                 selectedSubtitleIndex = position; | ||||
|                 break; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -286,11 +356,14 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|  | ||||
|         final RadioButton audioButton = radioVideoAudioGroup.findViewById(R.id.audio_button); | ||||
|         final RadioButton videoButton = radioVideoAudioGroup.findViewById(R.id.video_button); | ||||
|         final RadioButton subtitleButton = radioVideoAudioGroup.findViewById(R.id.subtitle_button); | ||||
|         final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0; | ||||
|         final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0; | ||||
|         final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0; | ||||
|  | ||||
|         audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE : View.GONE); | ||||
|         videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE); | ||||
|         subtitleButton.setVisibility(isSubtitleStreamsAvailable ? View.VISIBLE : View.GONE); | ||||
|  | ||||
|         if (isVideoStreamsAvailable) { | ||||
|             videoButton.setChecked(true); | ||||
| @@ -298,6 +371,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|         } else if (isAudioStreamsAvailable) { | ||||
|             audioButton.setChecked(true); | ||||
|             setupAudioSpinner(); | ||||
|         } else if (isSubtitleStreamsAvailable) { | ||||
|             subtitleButton.setChecked(true); | ||||
|             setupSubtitleSpinner(); | ||||
|         } else { | ||||
|             Toast.makeText(getContext(), R.string.no_streams_available_download, Toast.LENGTH_SHORT).show(); | ||||
|             getDialog().dismiss(); | ||||
| @@ -307,28 +383,144 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | ||||
|     private void setRadioButtonsState(boolean enabled) { | ||||
|         radioVideoAudioGroup.findViewById(R.id.audio_button).setEnabled(enabled); | ||||
|         radioVideoAudioGroup.findViewById(R.id.video_button).setEnabled(enabled); | ||||
|         radioVideoAudioGroup.findViewById(R.id.subtitle_button).setEnabled(enabled); | ||||
|     } | ||||
|  | ||||
|     private void downloadSelected() { | ||||
|     private int getSubtitleIndexBy(List<SubtitlesStream> streams) { | ||||
|         Localization loc = NewPipe.getLocalization(); | ||||
|         for (int j = 0; j < 2; j++) { | ||||
|             for (int i = 0; i < streams.size(); i++) { | ||||
|                 Locale streamLocale = streams.get(i).getLocale(); | ||||
|                 if (streamLocale.getLanguage().equals(loc.getLanguage())) { | ||||
|                     if (j > 0 || streamLocale.getCountry().equals(loc.getCountry())) { | ||||
|                         return i; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return 0; | ||||
|     } | ||||
|  | ||||
|     private void prepareSelectedDownload() { | ||||
|         final Context context = getContext(); | ||||
|         Stream stream; | ||||
|         String location; | ||||
|         char kind; | ||||
|  | ||||
|         String fileName = nameEditText.getText().toString().trim(); | ||||
|         if (fileName.isEmpty()) fileName = FilenameUtils.createFilename(getContext(), currentInfo.getName()); | ||||
|         if (fileName.isEmpty()) | ||||
|             fileName = FilenameUtils.createFilename(context, currentInfo.getName()); | ||||
|  | ||||
|         boolean isAudio = radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button; | ||||
|         if (isAudio) { | ||||
|         switch (radioVideoAudioGroup.getCheckedRadioButtonId()) { | ||||
|             case R.id.audio_button: | ||||
|                 stream = audioStreamsAdapter.getItem(selectedAudioIndex); | ||||
|             location = NewPipeSettings.getAudioDownloadPath(getContext()); | ||||
|         } else { | ||||
|                 location = NewPipeSettings.getAudioDownloadPath(context); | ||||
|                 kind = 'a'; | ||||
|                 break; | ||||
|             case R.id.video_button: | ||||
|                 stream = videoStreamsAdapter.getItem(selectedVideoIndex); | ||||
|             location = NewPipeSettings.getVideoDownloadPath(getContext()); | ||||
|                 location = NewPipeSettings.getVideoDownloadPath(context); | ||||
|                 kind = 'v'; | ||||
|                 break; | ||||
|             case R.id.subtitle_button: | ||||
|                 stream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex); | ||||
|                 location = NewPipeSettings.getVideoDownloadPath(context);// assume that subtitle & video go together | ||||
|                 kind = 's'; | ||||
|                 break; | ||||
|             default: | ||||
|                 return; | ||||
|         } | ||||
|  | ||||
|         String url = stream.getUrl(); | ||||
|         fileName += "." + stream.getFormat().getSuffix(); | ||||
|         int threads; | ||||
|  | ||||
|         if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) { | ||||
|             threads = 1;// use unique thread for subtitles due small file size | ||||
|             fileName += ".srt";// final subtitle format | ||||
|         } else { | ||||
|             threads = threadsSeekBar.getProgress() + 1; | ||||
|             fileName += "." + stream.getFormat().getSuffix(); | ||||
|         } | ||||
|  | ||||
|         final String finalFileName = fileName; | ||||
|  | ||||
|         DownloadManagerService.checkForRunningMission(context, location, fileName, (listed, finished) -> { | ||||
|             // should be safe run the following code without "getActivity().runOnUiThread()" | ||||
|             if (listed) { | ||||
|                 AlertDialog.Builder builder = new AlertDialog.Builder(context); | ||||
|                 builder.setTitle(R.string.download_dialog_title) | ||||
|                         .setMessage(finished ? R.string.overwrite_warning : R.string.download_already_running) | ||||
|                         .setPositiveButton( | ||||
|                                 finished ? R.string.overwrite : R.string.generate_unique_name, | ||||
|                                 (dialog, which) -> downloadSelected(context, stream, location, finalFileName, kind, threads) | ||||
|                         ) | ||||
|                         .setNegativeButton(android.R.string.cancel, (dialog, which) -> { | ||||
|                             dialog.cancel(); | ||||
|                         }) | ||||
|                         .create() | ||||
|                         .show(); | ||||
|             } else { | ||||
|                 downloadSelected(context, stream, location, finalFileName, kind, threads); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     private void downloadSelected(Context context, Stream selectedStream, String location, String fileName, char kind, int threads) { | ||||
|         String[] urls; | ||||
|         String psName = null; | ||||
|         String[] psArgs = null; | ||||
|         String secondaryStream = null; | ||||
|  | ||||
|         if (selectedStream instanceof VideoStream) { | ||||
|             VideoStream videoStream = (VideoStream) selectedStream; | ||||
|             if (videoStream.isVideoOnly() && videoStream.getFormat() != MediaFormat.v3GPP) { | ||||
|                 boolean m4v = videoStream.getFormat() == MediaFormat.MPEG_4; | ||||
|  | ||||
|                 for (AudioStream audio : audioStreamsAdapter.getAll()) { | ||||
|                     if (audio.getFormat() == (m4v ? MediaFormat.M4A : MediaFormat.WEBMA)) { | ||||
|                         secondaryStream = audio.getUrl(); | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if (secondaryStream == null) { | ||||
|                     // retry, but this time in reverse order | ||||
|                     List<AudioStream> audioStreams = audioStreamsAdapter.getAll(); | ||||
|                     for (int i = audioStreams.size() - 1; i >= 0; i--) { | ||||
|                         AudioStream audio = audioStreams.get(i); | ||||
|                         if (audio.getFormat() == (m4v ? MediaFormat.MP3 : MediaFormat.OPUS)) { | ||||
|                             secondaryStream = audio.getUrl(); | ||||
|                             break; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if (secondaryStream == null) { | ||||
|                     Log.w(TAG, "No audio stream candidates for video format " + videoStream.getFormat().name()); | ||||
|                     psName = null; | ||||
|                     psArgs = null; | ||||
|                 } else { | ||||
|                     psName = m4v ? Postprocessing.ALGORITHM_MP4_DASH_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER; | ||||
|                     psArgs = null; | ||||
|                 } | ||||
|             } | ||||
|         } else if ((selectedStream instanceof SubtitlesStream) && selectedStream.getFormat() == MediaFormat.TTML) { | ||||
|             psName = Postprocessing.ALGORITHM_TTML_CONVERTER; | ||||
|             psArgs = new String[]{ | ||||
|                     selectedStream.getFormat().getSuffix(), | ||||
|                     "false",//ignore empty frames | ||||
|                     "false",// detect youtube duplicateLines | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         if (secondaryStream == null) { | ||||
|             urls = new String[]{selectedStream.getUrl()}; | ||||
|         } else { | ||||
|             urls = new String[]{selectedStream.getUrl(), secondaryStream}; | ||||
|         } | ||||
|  | ||||
|         DownloadManagerService.startMission(context, urls, location, fileName, kind, threads, currentInfo.getUrl(), psName, psArgs); | ||||
|  | ||||
|         DownloadManagerService.startMission(getContext(), url, location, fileName, isAudio, threadsSeekBar.getProgress() + 1); | ||||
|         getDialog().dismiss(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -63,6 +63,7 @@ import org.schabi.newpipe.extractor.stream.Stream; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||
| import org.schabi.newpipe.extractor.stream.StreamType; | ||||
| import org.schabi.newpipe.extractor.stream.SubtitlesStream; | ||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | ||||
| import org.schabi.newpipe.fragments.BackPressable; | ||||
| import org.schabi.newpipe.fragments.BaseStateFragment; | ||||
| @@ -571,9 +572,6 @@ public class VideoDetailFragment | ||||
|                                 .show(getFragmentManager(), TAG); | ||||
|                     } | ||||
|                     break; | ||||
|                 case 3: | ||||
|                     shareUrl(item.getName(), item.getUrl()); | ||||
|                     break; | ||||
|                 default: | ||||
|                     break; | ||||
|             } | ||||
| @@ -745,7 +743,7 @@ public class VideoDetailFragment | ||||
|         sortedVideoStreams = ListHelper.getSortedStreamVideosList(activity, info.getVideoStreams(), info.getVideoOnlyStreams(), false); | ||||
|         selectedVideoStreamIndex = ListHelper.getDefaultResolutionIndex(activity, sortedVideoStreams); | ||||
|  | ||||
|         final StreamItemAdapter<VideoStream> streamsAdapter = new StreamItemAdapter<>(activity, new StreamSizeWrapper<>(sortedVideoStreams), isExternalPlayerEnabled); | ||||
|         final StreamItemAdapter<VideoStream> streamsAdapter = new StreamItemAdapter<>(activity, new StreamSizeWrapper<>(sortedVideoStreams, activity), isExternalPlayerEnabled); | ||||
|         spinnerToolbar.setAdapter(streamsAdapter); | ||||
|         spinnerToolbar.setSelection(selectedVideoStreamIndex); | ||||
|         spinnerToolbar.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { | ||||
| @@ -1276,6 +1274,7 @@ public class VideoDetailFragment | ||||
|                 downloadDialog.setVideoStreams(sortedVideoStreams); | ||||
|                 downloadDialog.setAudioStreams(currentInfo.getAudioStreams()); | ||||
|                 downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex); | ||||
|                 downloadDialog.setSubtitleStreams(currentInfo.getSubtitles()); | ||||
|  | ||||
|                 downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); | ||||
|             } catch (Exception e) { | ||||
|   | ||||
| @@ -19,11 +19,11 @@ import com.google.android.exoplayer2.util.MimeTypes; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.extractor.InfoItem; | ||||
| import org.schabi.newpipe.extractor.Subtitles; | ||||
| import org.schabi.newpipe.extractor.MediaFormat; | ||||
| import org.schabi.newpipe.extractor.stream.AudioStream; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||
| import org.schabi.newpipe.extractor.stream.SubtitlesFormat; | ||||
| import org.schabi.newpipe.extractor.stream.SubtitlesStream; | ||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueue; | ||||
| import org.schabi.newpipe.player.playqueue.PlayQueueItem; | ||||
| @@ -87,7 +87,7 @@ public class PlayerHelper { | ||||
|         return pitchFormatter.format(pitch); | ||||
|     } | ||||
|  | ||||
|     public static String mimeTypesOf(final SubtitlesFormat format) { | ||||
|     public static String subtitleMimeTypesOf(final MediaFormat format) { | ||||
|         switch (format) { | ||||
|             case VTT: return MimeTypes.TEXT_VTT; | ||||
|             case TTML: return MimeTypes.APPLICATION_TTML; | ||||
| @@ -97,8 +97,8 @@ public class PlayerHelper { | ||||
|  | ||||
|     @NonNull | ||||
|     public static String captionLanguageOf(@NonNull final Context context, | ||||
|                                            @NonNull final Subtitles subtitles) { | ||||
|         final String displayName = subtitles.getLocale().getDisplayName(subtitles.getLocale()); | ||||
|                                            @NonNull final SubtitlesStream subtitles) { | ||||
|         final String displayName = subtitles.getDisplayLanguageName(); | ||||
|         return displayName + (subtitles.isAutoGenerated() ? " (" + context.getString(R.string.caption_auto_generated)+ ")" : ""); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -10,10 +10,10 @@ import com.google.android.exoplayer2.source.MediaSource; | ||||
| import com.google.android.exoplayer2.source.MergingMediaSource; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.MediaFormat; | ||||
| import org.schabi.newpipe.extractor.Subtitles; | ||||
| import org.schabi.newpipe.extractor.stream.AudioStream; | ||||
| import org.schabi.newpipe.extractor.stream.StreamInfo; | ||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | ||||
| import org.schabi.newpipe.extractor.stream.SubtitlesStream; | ||||
| import org.schabi.newpipe.player.helper.PlayerDataSource; | ||||
| import org.schabi.newpipe.player.helper.PlayerHelper; | ||||
| import org.schabi.newpipe.util.ListHelper; | ||||
| @@ -93,8 +93,8 @@ public class VideoPlaybackResolver implements PlaybackResolver { | ||||
|         // Below are auxiliary media sources | ||||
|  | ||||
|         // Create subtitle sources | ||||
|         for (final Subtitles subtitle : info.getSubtitles()) { | ||||
|             final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType()); | ||||
|         for (final SubtitlesStream subtitle : info.getSubtitles()) { | ||||
|             final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat()); | ||||
|             if (mimeType == null) continue; | ||||
|  | ||||
|             final Format textFormat = Format.createTextSampleFormat(null, mimeType, | ||||
|   | ||||
| @@ -13,6 +13,7 @@ import org.schabi.newpipe.Downloader; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.extractor.stream.AudioStream; | ||||
| import org.schabi.newpipe.extractor.stream.Stream; | ||||
| import org.schabi.newpipe.extractor.stream.SubtitlesStream; | ||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | ||||
|  | ||||
| import java.io.Serializable; | ||||
| @@ -94,12 +95,25 @@ public class StreamItemAdapter<T extends Stream> extends BaseAdapter { | ||||
|             if (!showIconNoAudio) { | ||||
|                 woSoundIconVisibility = View.GONE; | ||||
|             } else if (((VideoStream) stream).isVideoOnly()) { | ||||
|                 switch (stream.getFormat()) { | ||||
|                     case WEBM:// fully supported | ||||
|                     case MPEG_4:// ¿is DASH MPEG-4? | ||||
|                         woSoundIconVisibility = View.INVISIBLE; | ||||
|                         break; | ||||
|                     default: | ||||
|                         woSoundIconVisibility = View.VISIBLE; | ||||
|                         break; | ||||
|                 } | ||||
|             } else if (isDropdownItem) { | ||||
|                 woSoundIconVisibility = View.INVISIBLE; | ||||
|             } | ||||
|         } else if (stream instanceof AudioStream) { | ||||
|             qualityString = ((AudioStream) stream).getAverageBitrate() + "kbps"; | ||||
|         } else if (stream instanceof SubtitlesStream) { | ||||
|             qualityString = ((SubtitlesStream) stream).getDisplayLanguageName(); | ||||
|             if (((SubtitlesStream) stream).isAutoGenerated()) { | ||||
|                 qualityString += " (" + context.getString(R.string.caption_auto_generated) + ")"; | ||||
|             } | ||||
|         } else { | ||||
|             qualityString = stream.getFormat().getSuffix(); | ||||
|         } | ||||
| @@ -111,7 +125,12 @@ public class StreamItemAdapter<T extends Stream> extends BaseAdapter { | ||||
|             sizeView.setVisibility(View.GONE); | ||||
|         } | ||||
|  | ||||
|         if (stream instanceof SubtitlesStream) { | ||||
|             formatNameView.setText(((SubtitlesStream) stream).getLanguageTag()); | ||||
|         } else { | ||||
|             formatNameView.setText(stream.getFormat().getName()); | ||||
|         } | ||||
|  | ||||
|         qualityView.setText(qualityString); | ||||
|         woSoundIconView.setVisibility(woSoundIconVisibility); | ||||
|  | ||||
| @@ -122,15 +141,17 @@ public class StreamItemAdapter<T extends Stream> extends BaseAdapter { | ||||
|      * A wrapper class that includes a way of storing the stream sizes. | ||||
|      */ | ||||
|     public static class StreamSizeWrapper<T extends Stream> implements Serializable { | ||||
|         private static final StreamSizeWrapper<Stream> EMPTY = new StreamSizeWrapper<>(Collections.emptyList()); | ||||
|         private static final StreamSizeWrapper<Stream> EMPTY = new StreamSizeWrapper<>(Collections.emptyList(), null); | ||||
|         private final List<T> streamsList; | ||||
|         private final long[] streamSizes; | ||||
|         private long[] streamSizes; | ||||
|         private final String unknownSize; | ||||
|  | ||||
|         public StreamSizeWrapper(List<T> streamsList) { | ||||
|         public StreamSizeWrapper(List<T> streamsList, Context context) { | ||||
|             this.streamsList = streamsList; | ||||
|             this.streamSizes = new long[streamsList.size()]; | ||||
|             this.unknownSize = context == null ? "--.-" : context.getString(R.string.unknown_content); | ||||
|  | ||||
|             for (int i = 0; i < streamSizes.length; i++) streamSizes[i] = -1; | ||||
|             for (int i = 0; i < streamSizes.length; i++) streamSizes[i] = -2; | ||||
|         } | ||||
|  | ||||
|         /** | ||||
| @@ -143,7 +164,7 @@ public class StreamItemAdapter<T extends Stream> extends BaseAdapter { | ||||
|             final Callable<Boolean> fetchAndSet = () -> { | ||||
|                 boolean hasChanged = false; | ||||
|                 for (X stream : streamsWrapper.getStreamsList()) { | ||||
|                     if (streamsWrapper.getSizeInBytes(stream) > 0) { | ||||
|                     if (streamsWrapper.getSizeInBytes(stream) > -2) { | ||||
|                         continue; | ||||
|                     } | ||||
|  | ||||
| @@ -173,11 +194,18 @@ public class StreamItemAdapter<T extends Stream> extends BaseAdapter { | ||||
|         } | ||||
|  | ||||
|         public String getFormattedSize(int streamIndex) { | ||||
|             return Utility.formatBytes(getSizeInBytes(streamIndex)); | ||||
|             return formatSize(getSizeInBytes(streamIndex)); | ||||
|         } | ||||
|  | ||||
|         public String getFormattedSize(T stream) { | ||||
|             return Utility.formatBytes(getSizeInBytes(stream)); | ||||
|             return formatSize(getSizeInBytes(stream)); | ||||
|         } | ||||
|  | ||||
|         private String formatSize(long size) { | ||||
|             if (size > -1) { | ||||
|                 return Utility.formatBytes(size); | ||||
|             } | ||||
|             return unknownSize; | ||||
|         } | ||||
|  | ||||
|         public void setSize(int streamIndex, long sizeInBytes) { | ||||
|   | ||||
							
								
								
									
										158
									
								
								app/src/main/java/us/shandian/giga/get/DownloadInitializer.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								app/src/main/java/us/shandian/giga/get/DownloadInitializer.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,158 @@ | ||||
| package us.shandian.giga.get; | ||||
|  | ||||
| import android.support.annotation.NonNull; | ||||
| import android.util.Log; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.io.RandomAccessFile; | ||||
| import java.net.HttpURLConnection; | ||||
| import java.nio.channels.ClosedByInterruptException; | ||||
|  | ||||
| import us.shandian.giga.util.Utility; | ||||
|  | ||||
| import static org.schabi.newpipe.BuildConfig.DEBUG; | ||||
|  | ||||
| public class DownloadInitializer implements Runnable { | ||||
|     private final static String TAG = "DownloadInitializer"; | ||||
|     final static int mId = 0; | ||||
|  | ||||
|     private DownloadMission mMission; | ||||
|  | ||||
|     DownloadInitializer(@NonNull DownloadMission mission) { | ||||
|         mMission = mission; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void run() { | ||||
|         if (mMission.current > 0) mMission.resetState(); | ||||
|  | ||||
|         int retryCount = 0; | ||||
|         while (true) { | ||||
|             try { | ||||
|                 mMission.currentThreadCount = mMission.threadCount; | ||||
|  | ||||
|                 HttpURLConnection conn = mMission.openConnection(mId, -1, -1); | ||||
|                 if (!mMission.running || Thread.interrupted()) return; | ||||
|  | ||||
|                 mMission.length = conn.getContentLength(); | ||||
|                 if (mMission.length == 0) { | ||||
|                     mMission.notifyError(DownloadMission.ERROR_HTTP_NO_CONTENT, null); | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 // check for dynamic generated content | ||||
|                 if (mMission.length == -1 && conn.getResponseCode() == 200) { | ||||
|                     mMission.blocks = 0; | ||||
|                     mMission.length = 0; | ||||
|                     mMission.fallback = true; | ||||
|                     mMission.unknownLength = true; | ||||
|                     mMission.currentThreadCount = 1; | ||||
|  | ||||
|                     if (DEBUG) { | ||||
|                         Log.d(TAG, "falling back (unknown length)"); | ||||
|                     } | ||||
|                 } else { | ||||
|                     // Open again | ||||
|                     conn = mMission.openConnection(mId, mMission.length - 10, mMission.length); | ||||
|  | ||||
|                     int code = conn.getResponseCode(); | ||||
|                     if (!mMission.running || Thread.interrupted()) return; | ||||
|  | ||||
|                     if (code == 206) { | ||||
|                         if (mMission.currentThreadCount > 1) { | ||||
|                             mMission.blocks = mMission.length / DownloadMission.BLOCK_SIZE; | ||||
|  | ||||
|                             if (mMission.currentThreadCount > mMission.blocks) { | ||||
|                                 mMission.currentThreadCount = (int) mMission.blocks; | ||||
|                             } | ||||
|                             if (mMission.currentThreadCount <= 0) { | ||||
|                                 mMission.currentThreadCount = 1; | ||||
|                             } | ||||
|                             if (mMission.blocks * DownloadMission.BLOCK_SIZE < mMission.length) { | ||||
|                                 mMission.blocks++; | ||||
|                             } | ||||
|                         } else { | ||||
|                             // if one thread is solicited don't calculate blocks, is useless | ||||
|                             mMission.blocks = 0; | ||||
|                             mMission.fallback = true; | ||||
|                             mMission.unknownLength = false; | ||||
|                         } | ||||
|  | ||||
|                         if (DEBUG) { | ||||
|                             Log.d(TAG, "http response code = " + code); | ||||
|                         } | ||||
|                     } else { | ||||
|                         // Fallback to single thread | ||||
|                         mMission.blocks = 0; | ||||
|                         mMission.fallback = true; | ||||
|                         mMission.unknownLength = false; | ||||
|                         mMission.currentThreadCount = 1; | ||||
|  | ||||
|                         if (DEBUG) { | ||||
|                             Log.d(TAG, "falling back due http response code = " + code); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 for (long i = 0; i < mMission.currentThreadCount; i++) { | ||||
|                     mMission.threadBlockPositions.add(i); | ||||
|                     mMission.threadBytePositions.add(0); | ||||
|                 } | ||||
|  | ||||
|                 File file; | ||||
|                 if (mMission.current == 0) { | ||||
|                     file = new File(mMission.location); | ||||
|                     if (!Utility.mkdir(file, true)) { | ||||
|                         mMission.notifyError(DownloadMission.ERROR_PATH_CREATION, null); | ||||
|                         return; | ||||
|                     } | ||||
|  | ||||
|                     file = new File(file, mMission.name); | ||||
|  | ||||
|                     // if the name is used by "something", delete it | ||||
|                     if (file.exists() && !file.isFile() && !file.delete()) { | ||||
|                         mMission.notifyError(DownloadMission.ERROR_FILE_CREATION, null); | ||||
|                         return; | ||||
|                     } | ||||
|  | ||||
|                     if (!file.exists() && !file.createNewFile()) { | ||||
|                         mMission.notifyError(DownloadMission.ERROR_FILE_CREATION, null); | ||||
|                         return; | ||||
|                     } | ||||
|                 } else { | ||||
|                     file = new File(mMission.location, mMission.name); | ||||
|                 } | ||||
|  | ||||
|                 RandomAccessFile af = new RandomAccessFile(file, "rw"); | ||||
|                 af.setLength(mMission.offsets[mMission.current] + mMission.length); | ||||
|                 af.seek(mMission.offsets[mMission.current]); | ||||
|                 af.close(); | ||||
|  | ||||
|                 if (Thread.interrupted()) return; | ||||
|  | ||||
|                 mMission.running = false; | ||||
|                 break; | ||||
|             } catch (Exception e) { | ||||
|                 if (e instanceof ClosedByInterruptException) { | ||||
|                     return; | ||||
|                 } else if (e instanceof IOException && e.getMessage().contains("Permission denied")) { | ||||
|                     mMission.notifyError(DownloadMission.ERROR_PERMISSION_DENIED, e); | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 if (retryCount++ > mMission.maxRetry) { | ||||
|                     Log.e(TAG, "initializer failed", e); | ||||
|                     mMission.running = false; | ||||
|                     mMission.notifyError(e); | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 //try again | ||||
|                 Log.e(TAG, "initializer failed, retrying", e); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         mMission.start(); | ||||
|     } | ||||
| } | ||||
| @@ -1,53 +0,0 @@ | ||||
| package us.shandian.giga.get; | ||||
|  | ||||
| public interface DownloadManager { | ||||
|     int BLOCK_SIZE = 512 * 1024; | ||||
|  | ||||
|     /** | ||||
|      * Start a new download mission | ||||
|      * | ||||
|      * @param url      the url to download | ||||
|      * @param location the location | ||||
|      * @param name     the name of the file to create | ||||
|      * @param isAudio  true if the download is an audio file | ||||
|      * @param threads  the number of threads maximal used to download chunks of the file.    @return the identifier of the mission. | ||||
|      */ | ||||
|     int startMission(String url, String location, String name, boolean isAudio, int threads); | ||||
|  | ||||
|     /** | ||||
|      * Resume the execution of a download mission. | ||||
|      * | ||||
|      * @param id the identifier of the mission to resume. | ||||
|      */ | ||||
|     void resumeMission(int id); | ||||
|  | ||||
|     /** | ||||
|      * Pause the execution of a download mission. | ||||
|      * | ||||
|      * @param id the identifier of the mission to pause. | ||||
|      */ | ||||
|     void pauseMission(int id); | ||||
|  | ||||
|     /** | ||||
|      * Deletes the mission from the downloaded list but keeps the downloaded file. | ||||
|      * | ||||
|      * @param id The mission identifier | ||||
|      */ | ||||
|     void deleteMission(int id); | ||||
|  | ||||
|     /** | ||||
|      * Get the download mission by its identifier | ||||
|      * | ||||
|      * @param id the identifier of the download mission | ||||
|      * @return the download mission or null if the mission doesn't exist | ||||
|      */ | ||||
|     DownloadMission getMission(int id); | ||||
|  | ||||
|     /** | ||||
|      * Get the number of download missions. | ||||
|      * | ||||
|      * @return the number of download missions. | ||||
|      */ | ||||
|     int getCount(); | ||||
|  | ||||
| } | ||||
| @@ -1,102 +1,165 @@ | ||||
| package us.shandian.giga.get; | ||||
|  | ||||
| import android.os.Handler; | ||||
| import android.os.Looper; | ||||
| import android.os.Message; | ||||
| import android.util.Log; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.ObjectInputStream; | ||||
| import java.io.Serializable; | ||||
| import java.lang.ref.WeakReference; | ||||
| import java.io.FileNotFoundException; | ||||
| import java.io.IOException; | ||||
| import java.net.ConnectException; | ||||
| import java.net.HttpURLConnection; | ||||
| import java.net.URL; | ||||
| import java.net.UnknownHostException; | ||||
| import java.util.ArrayList; | ||||
| import java.util.HashMap; | ||||
| import java.util.Iterator; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
|  | ||||
| import javax.net.ssl.SSLException; | ||||
|  | ||||
| import us.shandian.giga.postprocessing.Postprocessing; | ||||
| import us.shandian.giga.service.DownloadManagerService; | ||||
| import us.shandian.giga.util.Utility; | ||||
|  | ||||
| import static org.schabi.newpipe.BuildConfig.DEBUG; | ||||
|  | ||||
| public class DownloadMission implements Serializable { | ||||
|     private static final long serialVersionUID = 0L; | ||||
| public class DownloadMission extends Mission { | ||||
|     private static final long serialVersionUID = 3L;// last bump: 16 october 2018 | ||||
|  | ||||
|     private static final String TAG = DownloadMission.class.getSimpleName(); | ||||
|     static final int BUFFER_SIZE = 64 * 1024; | ||||
|     final static int BLOCK_SIZE = 512 * 1024; | ||||
|  | ||||
|     public interface MissionListener { | ||||
|         HashMap<MissionListener, Handler> handlerStore = new HashMap<>(); | ||||
|     private static final String TAG = "DownloadMission"; | ||||
|  | ||||
|         void onProgressUpdate(DownloadMission downloadMission, long done, long total); | ||||
|  | ||||
|         void onFinish(DownloadMission downloadMission); | ||||
|  | ||||
|         void onError(DownloadMission downloadMission, int errCode); | ||||
|     } | ||||
|  | ||||
|     public static final int ERROR_SERVER_UNSUPPORTED = 206; | ||||
|     public static final int ERROR_UNKNOWN = 233; | ||||
|     public static final int ERROR_NOTHING = -1; | ||||
|     public static final int ERROR_PATH_CREATION = 1000; | ||||
|     public static final int ERROR_FILE_CREATION = 1001; | ||||
|     public static final int ERROR_UNKNOWN_EXCEPTION = 1002; | ||||
|     public static final int ERROR_PERMISSION_DENIED = 1003; | ||||
|     public static final int ERROR_SSL_EXCEPTION = 1004; | ||||
|     public static final int ERROR_UNKNOWN_HOST = 1005; | ||||
|     public static final int ERROR_CONNECT_HOST = 1006; | ||||
|     public static final int ERROR_POSTPROCESSING_FAILED = 1007; | ||||
|     public static final int ERROR_HTTP_NO_CONTENT = 204; | ||||
|     public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206; | ||||
|  | ||||
|     /** | ||||
|      * The filename | ||||
|      * The urls of the file to download | ||||
|      */ | ||||
|     public String name; | ||||
|     public String[] urls; | ||||
|  | ||||
|     /** | ||||
|      * The url of the file to download | ||||
|      * Number of blocks the size of {@link DownloadMission#BLOCK_SIZE} | ||||
|      */ | ||||
|     public String url; | ||||
|  | ||||
|     /** | ||||
|      * The directory to store the download | ||||
|      */ | ||||
|     public String location; | ||||
|  | ||||
|     /** | ||||
|      * Number of blocks the size of {@link DownloadManager#BLOCK_SIZE} | ||||
|      */ | ||||
|     public long blocks; | ||||
|  | ||||
|     /** | ||||
|      * Number of bytes | ||||
|      */ | ||||
|     public long length; | ||||
|     long blocks = -1; | ||||
|  | ||||
|     /** | ||||
|      * Number of bytes downloaded | ||||
|      */ | ||||
|     public long done; | ||||
|  | ||||
|     /** | ||||
|      * Indicates a file generated dynamically on the web server | ||||
|      */ | ||||
|     public boolean unknownLength; | ||||
|  | ||||
|     /** | ||||
|      * offset in the file where the data should be written | ||||
|      */ | ||||
|     public long[] offsets; | ||||
|  | ||||
|     /** | ||||
|      * The post-processing algorithm arguments | ||||
|      */ | ||||
|     public String[] postprocessingArgs; | ||||
|  | ||||
|     /** | ||||
|      * The post-processing algorithm name | ||||
|      */ | ||||
|     public String postprocessingName; | ||||
|  | ||||
|     /** | ||||
|      * Indicates if the post-processing algorithm is actually running, used to detect corrupt downloads | ||||
|      */ | ||||
|     public boolean postprocessingRunning; | ||||
|  | ||||
|     /** | ||||
|      * Indicate if the post-processing algorithm works on the same file | ||||
|      */ | ||||
|     public boolean postprocessingThis; | ||||
|  | ||||
|     /** | ||||
|      * The current resource to download {@code urls[current]} | ||||
|      */ | ||||
|     public int current; | ||||
|  | ||||
|     /** | ||||
|      * Metadata where the mission state is saved | ||||
|      */ | ||||
|     public File metadata; | ||||
|  | ||||
|     /** | ||||
|      * maximum attempts | ||||
|      */ | ||||
|     public int maxRetry; | ||||
|  | ||||
|     public int threadCount = 3; | ||||
|     public int finishCount; | ||||
|     private final List<Long> threadPositions = new ArrayList<>(); | ||||
|     public final Map<Long, Boolean> blockState = new HashMap<>(); | ||||
|     public boolean running; | ||||
|     public boolean finished; | ||||
|     public boolean fallback; | ||||
|     public int errCode = -1; | ||||
|     public long timestamp; | ||||
|     boolean fallback; | ||||
|     private int finishCount; | ||||
|     public transient boolean running; | ||||
|     public transient boolean enqueued = true; | ||||
|  | ||||
|     public int errCode = ERROR_NOTHING; | ||||
|  | ||||
|     public transient Exception errObject = null; | ||||
|     public transient boolean recovered; | ||||
|  | ||||
|     private transient ArrayList<WeakReference<MissionListener>> mListeners = new ArrayList<>(); | ||||
|     public transient Handler mHandler; | ||||
|     private transient boolean mWritingToFile; | ||||
|  | ||||
|     private static final int NO_IDENTIFIER = -1; | ||||
|     @SuppressWarnings("UseSparseArrays")// LongSparseArray is not serializable | ||||
|     private final HashMap<Long, Boolean> blockState = new HashMap<>(); | ||||
|     final List<Long> threadBlockPositions = new ArrayList<>(); | ||||
|     final List<Integer> threadBytePositions = new ArrayList<>(); | ||||
|  | ||||
|     private transient boolean deleted; | ||||
|     int currentThreadCount; | ||||
|     private transient Thread[] threads = null; | ||||
|     private transient Thread init = null; | ||||
|  | ||||
|  | ||||
|     protected DownloadMission() { | ||||
|  | ||||
|     public DownloadMission() { | ||||
|     } | ||||
|  | ||||
|     public DownloadMission(String name, String url, String location) { | ||||
|     public DownloadMission(String url, String name, String location, char kind) { | ||||
|         this(new String[]{url}, name, location, kind, null, null); | ||||
|     } | ||||
|  | ||||
|     public DownloadMission(String[] urls, String name, String location, char kind, String postprocessingName, String[] postprocessingArgs) { | ||||
|         if (name == null) throw new NullPointerException("name is null"); | ||||
|         if (name.isEmpty()) throw new IllegalArgumentException("name is empty"); | ||||
|         if (url == null) throw new NullPointerException("url is null"); | ||||
|         if (url.isEmpty()) throw new IllegalArgumentException("url is empty"); | ||||
|         if (urls == null) throw new NullPointerException("urls is null"); | ||||
|         if (urls.length < 1) throw new IllegalArgumentException("urls is empty"); | ||||
|         if (location == null) throw new NullPointerException("location is null"); | ||||
|         if (location.isEmpty()) throw new IllegalArgumentException("location is empty"); | ||||
|         this.url = url; | ||||
|         this.urls = urls; | ||||
|         this.name = name; | ||||
|         this.location = location; | ||||
|     } | ||||
|         this.kind = kind; | ||||
|         this.offsets = new long[urls.length]; | ||||
|  | ||||
|         if (postprocessingName != null) { | ||||
|             Postprocessing algorithm = Postprocessing.getAlgorithm(postprocessingName, null); | ||||
|             this.postprocessingThis = algorithm.worksOnSameFile; | ||||
|             this.offsets[0] = algorithm.recommendedReserve; | ||||
|             this.postprocessingName = postprocessingName; | ||||
|             this.postprocessingArgs = postprocessingArgs; | ||||
|         } else { | ||||
|             if (DEBUG && urls.length > 1) { | ||||
|                 Log.w(TAG, "mission created with multiple urls ¿missing post-processing algorithm?"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void checkBlock(long block) { | ||||
|         if (block < 0 || block >= blocks) { | ||||
| @@ -110,12 +173,12 @@ public class DownloadMission implements Serializable { | ||||
|      * @param block the block identifier | ||||
|      * @return true if the block is reserved and false if otherwise | ||||
|      */ | ||||
|     public boolean isBlockPreserved(long block) { | ||||
|     boolean isBlockPreserved(long block) { | ||||
|         checkBlock(block); | ||||
|         return blockState.containsKey(block) ? blockState.get(block) : false; | ||||
|     } | ||||
|  | ||||
|     public void preserveBlock(long block) { | ||||
|     void preserveBlock(long block) { | ||||
|         checkBlock(block); | ||||
|         synchronized (blockState) { | ||||
|             blockState.put(block, true); | ||||
| @@ -123,125 +186,192 @@ public class DownloadMission implements Serializable { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Set the download position of the file | ||||
|      * Set the block of the file | ||||
|      * | ||||
|      * @param threadId the identifier of the thread | ||||
|      * @param position the download position of the thread | ||||
|      * @param position the block of the thread | ||||
|      */ | ||||
|     public void setPosition(int threadId, long position) { | ||||
|         threadPositions.set(threadId, position); | ||||
|     void setBlockPosition(int threadId, long position) { | ||||
|         threadBlockPositions.set(threadId, position); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the position of a thread | ||||
|      * Get the block of a file | ||||
|      * | ||||
|      * @param threadId the identifier of the thread | ||||
|      * @return the position for the thread | ||||
|      * @return the block for the thread | ||||
|      */ | ||||
|     public long getPosition(int threadId) { | ||||
|         return threadPositions.get(threadId); | ||||
|     long getBlockPosition(int threadId) { | ||||
|         return threadBlockPositions.get(threadId); | ||||
|     } | ||||
|  | ||||
|     public synchronized void notifyProgress(long deltaLen) { | ||||
|     /** | ||||
|      * Save the position of the desired thread | ||||
|      * | ||||
|      * @param threadId the identifier of the thread | ||||
|      * @param position the relative position in bytes or zero | ||||
|      */ | ||||
|     void setThreadBytePosition(int threadId, int position) { | ||||
|         threadBytePositions.set(threadId, position); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get position inside of the block, where thread will be resumed | ||||
|      * | ||||
|      * @param threadId the identifier of the thread | ||||
|      * @return the relative position in bytes or zero | ||||
|      */ | ||||
|     int getBlockBytePosition(int threadId) { | ||||
|         return threadBytePositions.get(threadId); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Open connection | ||||
|      * | ||||
|      * @param threadId   id of the calling thread, used only for debug | ||||
|      * @param rangeStart range start | ||||
|      * @param rangeEnd   range end | ||||
|      * @return a {@link java.net.URLConnection URLConnection} linking to the URL. | ||||
|      * @throws IOException if an I/O exception occurs. | ||||
|      * @throws HttpError   if the the http response is not satisfiable | ||||
|      */ | ||||
|     HttpURLConnection openConnection(int threadId, long rangeStart, long rangeEnd) throws IOException, HttpError { | ||||
|         URL url = new URL(urls[current]); | ||||
|         HttpURLConnection conn = (HttpURLConnection) url.openConnection(); | ||||
|         conn.setInstanceFollowRedirects(true); | ||||
|  | ||||
|         if (rangeStart >= 0) { | ||||
|             String req = "bytes=" + rangeStart + "-"; | ||||
|             if (rangeEnd > 0) req += rangeEnd; | ||||
|  | ||||
|             conn.setRequestProperty("Range", req); | ||||
|             if (DEBUG) { | ||||
|                 Log.d(TAG, threadId + ":" + conn.getRequestProperty("Range")); | ||||
|                 Log.d(TAG, threadId + ":Content-Length=" + conn.getContentLength() + " Code:" + conn.getResponseCode()); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         int statusCode = conn.getResponseCode(); | ||||
|         switch (statusCode) { | ||||
|             case 204: | ||||
|             case 205: | ||||
|             case 207: | ||||
|                 throw new HttpError(conn.getResponseCode()); | ||||
|             default: | ||||
|                 if (statusCode < 200 || statusCode > 299) { | ||||
|                     throw new HttpError(statusCode); | ||||
|                 } | ||||
|         } | ||||
|  | ||||
|         return conn; | ||||
|     } | ||||
|  | ||||
|     private void notify(int what) { | ||||
|         Message m = new Message(); | ||||
|         m.what = what; | ||||
|         m.obj = this; | ||||
|  | ||||
|         mHandler.sendMessage(m); | ||||
|     } | ||||
|  | ||||
|     synchronized void notifyProgress(long deltaLen) { | ||||
|         if (!running) return; | ||||
|  | ||||
|         if (recovered) { | ||||
|             recovered = false; | ||||
|         } | ||||
|  | ||||
|         if (unknownLength) { | ||||
|             length += deltaLen;// Update length before proceeding | ||||
|         } | ||||
|  | ||||
|         done += deltaLen; | ||||
|  | ||||
|         if (done > length) { | ||||
|             done = length; | ||||
|         } | ||||
|  | ||||
|         if (done != length) { | ||||
|             writeThisToFile(); | ||||
|         if (done != length && !deleted && !mWritingToFile) { | ||||
|             mWritingToFile = true; | ||||
|             runAsync(-2, this::writeThisToFile); | ||||
|         } | ||||
|  | ||||
|         for (WeakReference<MissionListener> ref : mListeners) { | ||||
|             final MissionListener listener = ref.get(); | ||||
|             if (listener != null) { | ||||
|                 MissionListener.handlerStore.get(listener).post(new Runnable() { | ||||
|                     @Override | ||||
|                     public void run() { | ||||
|                         listener.onProgressUpdate(DownloadMission.this, done, length); | ||||
|                     } | ||||
|                 }); | ||||
|         notify(DownloadManagerService.MESSAGE_PROGRESS); | ||||
|     } | ||||
|  | ||||
|     synchronized void notifyError(Exception err) { | ||||
|         Log.e(TAG, "notifyError()", err); | ||||
|  | ||||
|         if (err instanceof FileNotFoundException) { | ||||
|             notifyError(ERROR_FILE_CREATION, null); | ||||
|         } else if (err instanceof SSLException) { | ||||
|             notifyError(ERROR_SSL_EXCEPTION, null); | ||||
|         } else if (err instanceof HttpError) { | ||||
|             notifyError(((HttpError) err).statusCode, null); | ||||
|         } else if (err instanceof ConnectException) { | ||||
|             notifyError(ERROR_CONNECT_HOST, null); | ||||
|         } else if (err instanceof UnknownHostException) { | ||||
|             notifyError(ERROR_UNKNOWN_HOST, null); | ||||
|         } else { | ||||
|             notifyError(ERROR_UNKNOWN_EXCEPTION, err); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called by a download thread when it finished. | ||||
|      */ | ||||
|     public synchronized void notifyFinished() { | ||||
|         if (errCode > 0) return; | ||||
|     synchronized void notifyError(int code, Exception err) { | ||||
|         Log.e(TAG, "notifyError() code = " + code, err); | ||||
|  | ||||
|         errCode = code; | ||||
|         errObject = err; | ||||
|  | ||||
|         pause(); | ||||
|  | ||||
|         notify(DownloadManagerService.MESSAGE_ERROR); | ||||
|     } | ||||
|  | ||||
|     synchronized void notifyFinished() { | ||||
|         if (errCode > ERROR_NOTHING) return; | ||||
|  | ||||
|         finishCount++; | ||||
|  | ||||
|         if (finishCount == threadCount) { | ||||
|             onFinish(); | ||||
|         } | ||||
|         if (finishCount == currentThreadCount) { | ||||
|             if ((current + 1) < urls.length) { | ||||
|                 // prepare next sub-mission | ||||
|                 long current_offset = offsets[current++]; | ||||
|                 offsets[current] = current_offset + length; | ||||
|                 initializer(); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|     /** | ||||
|      * Called when all parts are downloaded | ||||
|      */ | ||||
|     private void onFinish() { | ||||
|         if (errCode > 0) return; | ||||
|             current++; | ||||
|             unknownLength = false; | ||||
|  | ||||
|             if (!doPostprocessing()) return; | ||||
|  | ||||
|             if (errCode > ERROR_NOTHING) return; | ||||
|             if (DEBUG) { | ||||
|                 Log.d(TAG, "onFinish"); | ||||
|             } | ||||
|  | ||||
|             running = false; | ||||
|         finished = true; | ||||
|  | ||||
|             deleteThisFromFile(); | ||||
|  | ||||
|         for (WeakReference<MissionListener> ref : mListeners) { | ||||
|             final MissionListener listener = ref.get(); | ||||
|             if (listener != null) { | ||||
|                 MissionListener.handlerStore.get(listener).post(new Runnable() { | ||||
|                     @Override | ||||
|                     public void run() { | ||||
|                         listener.onFinish(DownloadMission.this); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public synchronized void notifyError(int err) { | ||||
|         errCode = err; | ||||
|  | ||||
|         writeThisToFile(); | ||||
|  | ||||
|         for (WeakReference<MissionListener> ref : mListeners) { | ||||
|             final MissionListener listener = ref.get(); | ||||
|             MissionListener.handlerStore.get(listener).post(new Runnable() { | ||||
|                 @Override | ||||
|                 public void run() { | ||||
|                     listener.onError(DownloadMission.this, errCode); | ||||
|                 } | ||||
|             }); | ||||
|             notify(DownloadManagerService.MESSAGE_FINISHED); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public synchronized void addListener(MissionListener listener) { | ||||
|         Handler handler = new Handler(Looper.getMainLooper()); | ||||
|         MissionListener.handlerStore.put(listener, handler); | ||||
|         mListeners.add(new WeakReference<>(listener)); | ||||
|     private void notifyPostProcessing(boolean processing) { | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, (processing ? "enter" : "exit") + " postprocessing on " + location + File.separator + name); | ||||
|         } | ||||
|  | ||||
|     public synchronized void removeListener(MissionListener listener) { | ||||
|         for (Iterator<WeakReference<MissionListener>> iterator = mListeners.iterator(); | ||||
|              iterator.hasNext(); ) { | ||||
|             WeakReference<MissionListener> weakRef = iterator.next(); | ||||
|             if (listener != null && listener == weakRef.get()) { | ||||
|                 iterator.remove(); | ||||
|         synchronized (blockState) { | ||||
|             if (!processing) { | ||||
|                 postprocessingName = null; | ||||
|                 postprocessingArgs = null; | ||||
|             } | ||||
|  | ||||
|             // don't return without fully write the current state | ||||
|             postprocessingRunning = processing; | ||||
|             Utility.writeToFile(metadata, DownloadMission.this); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -249,92 +379,206 @@ public class DownloadMission implements Serializable { | ||||
|      * Start downloading with multiple threads. | ||||
|      */ | ||||
|     public void start() { | ||||
|         if (!running && !finished) { | ||||
|         if (running || current >= urls.length) return; | ||||
|         enqueued = false; | ||||
|         running = true; | ||||
|         errCode = ERROR_NOTHING; | ||||
|  | ||||
|             if (!fallback) { | ||||
|                 for (int i = 0; i < threadCount; i++) { | ||||
|                     if (threadPositions.size() <= i && !recovered) { | ||||
|                         threadPositions.add((long) i); | ||||
|         if (blocks < 0) { | ||||
|             initializer(); | ||||
|             return; | ||||
|         } | ||||
|                     new Thread(new DownloadRunnable(this, i)).start(); | ||||
|  | ||||
|         init = null; | ||||
|  | ||||
|         if (threads == null) { | ||||
|             threads = new Thread[currentThreadCount]; | ||||
|         } | ||||
|             } else { | ||||
|                 // In fallback mode, resuming is not supported. | ||||
|                 threadCount = 1; | ||||
|  | ||||
|         if (fallback) { | ||||
|             if (unknownLength) { | ||||
|                 done = 0; | ||||
|                 blocks = 0; | ||||
|                 new Thread(new DownloadRunnableFallback(this)).start(); | ||||
|                 length = 0; | ||||
|             } | ||||
|  | ||||
|             threads[0] = runAsync(1, new DownloadRunnableFallback(this)); | ||||
|         } else { | ||||
|             for (int i = 0; i < currentThreadCount; i++) { | ||||
|                 threads[i] = runAsync(i + 1, new DownloadRunnable(this, i)); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void pause() { | ||||
|         if (running) { | ||||
|     /** | ||||
|      * Pause the mission, does not affect the blocks that are being downloaded. | ||||
|      */ | ||||
|     public synchronized void pause() { | ||||
|         if (!running) return; | ||||
|  | ||||
|         running = false; | ||||
|         recovered = true; | ||||
|         enqueued = false; | ||||
|  | ||||
|             // TODO: Notify & Write state to info file | ||||
|             // if (err) | ||||
|         if (init != null && init != Thread.currentThread() && init.isAlive()) { | ||||
|             init.interrupt(); | ||||
|  | ||||
|             try { | ||||
|                 init.join(); | ||||
|             } catch (InterruptedException e) { | ||||
|                 // nothing to do | ||||
|             } | ||||
|  | ||||
|             resetState(); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (DEBUG && blocks < 1) { | ||||
|             Log.w(TAG, "pausing a download that can not be resumed."); | ||||
|         } | ||||
|  | ||||
|         if (threads == null || Thread.interrupted()) { | ||||
|             writeThisToFile(); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // wait for all threads are suspended before save the state | ||||
|         runAsync(-1, () -> { | ||||
|             try { | ||||
|                 for (Thread thread : threads) { | ||||
|                     if (thread == Thread.currentThread()) continue; | ||||
|  | ||||
|                     if (thread.isAlive()) { | ||||
|                         thread.interrupt(); | ||||
|                         thread.join(); | ||||
|                     } | ||||
|                 } | ||||
|             } catch (Exception e) { | ||||
|                 // nothing to do | ||||
|             } finally { | ||||
|                 writeThisToFile(); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Removes the file and the meta file | ||||
|      */ | ||||
|     public void delete() { | ||||
|         deleteThisFromFile(); | ||||
|         new File(location, name).delete(); | ||||
|     @Override | ||||
|     public boolean delete() { | ||||
|         deleted = true; | ||||
|         boolean res = deleteThisFromFile(); | ||||
|         if (!super.delete()) res = false; | ||||
|         return res; | ||||
|     } | ||||
|  | ||||
|     void resetState() { | ||||
|         done = 0; | ||||
|         blocks = -1; | ||||
|         errCode = ERROR_NOTHING; | ||||
|         fallback = false; | ||||
|         unknownLength = false; | ||||
|         finishCount = 0; | ||||
|         threadBlockPositions.clear(); | ||||
|         threadBytePositions.clear(); | ||||
|         blockState.clear(); | ||||
|         threads = null; | ||||
|  | ||||
|         Utility.writeToFile(metadata, DownloadMission.this); | ||||
|     } | ||||
|  | ||||
|     private void initializer() { | ||||
|         init = runAsync(DownloadInitializer.mId, new DownloadInitializer(this)); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Write this {@link DownloadMission} to the meta file asynchronously | ||||
|      * if no thread is already running. | ||||
|      */ | ||||
|     public void writeThisToFile() { | ||||
|         if (!mWritingToFile) { | ||||
|             mWritingToFile = true; | ||||
|             new Thread() { | ||||
|                 @Override | ||||
|                 public void run() { | ||||
|                     doWriteThisToFile(); | ||||
|     private void writeThisToFile() { | ||||
|         synchronized (blockState) { | ||||
|             if (deleted) return; | ||||
|             Utility.writeToFile(metadata, DownloadMission.this); | ||||
|         } | ||||
|         mWritingToFile = false; | ||||
|     } | ||||
|             }.start(); | ||||
|         } | ||||
|  | ||||
|     public boolean isFinished() { | ||||
|         return current >= urls.length && postprocessingName == null; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Write this {@link DownloadMission} to the meta file. | ||||
|      */ | ||||
|     private void doWriteThisToFile() { | ||||
|     private boolean doPostprocessing() { | ||||
|         if (postprocessingName == null) return true; | ||||
|  | ||||
|         try { | ||||
|             notifyPostProcessing(true); | ||||
|             notifyProgress(0); | ||||
|  | ||||
|             Thread.currentThread().setName("[" + TAG + "]  post-processing = " + postprocessingName + "  filename = " + name); | ||||
|  | ||||
|             Postprocessing algorithm = Postprocessing.getAlgorithm(postprocessingName, this); | ||||
|             algorithm.run(); | ||||
|         } catch (Exception err) { | ||||
|             StringBuilder args = new StringBuilder("  "); | ||||
|             if (postprocessingArgs != null) { | ||||
|                 for (String arg : postprocessingArgs) { | ||||
|                     args.append(", "); | ||||
|                     args.append(arg); | ||||
|                 } | ||||
|                 args.delete(0, 1); | ||||
|             } | ||||
|             Log.e(TAG, String.format("Post-processing failed. algorithm = %s  args = [%s]", postprocessingName, args), err); | ||||
|  | ||||
|             notifyError(ERROR_POSTPROCESSING_FAILED, err); | ||||
|             return false; | ||||
|         } finally { | ||||
|             notifyPostProcessing(false); | ||||
|         } | ||||
|  | ||||
|         if (errCode != ERROR_NOTHING) notify(DownloadManagerService.MESSAGE_ERROR); | ||||
|  | ||||
|         return errCode == ERROR_NOTHING; | ||||
|     } | ||||
|  | ||||
|     private boolean deleteThisFromFile() { | ||||
|         synchronized (blockState) { | ||||
|             Utility.writeToFile(getMetaFilename(), this); | ||||
|             return metadata.delete(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void readObject(ObjectInputStream inputStream) | ||||
|     throws java.io.IOException, ClassNotFoundException | ||||
|     { | ||||
|         inputStream.defaultReadObject(); | ||||
|         mListeners = new ArrayList<>(); | ||||
|     } | ||||
|  | ||||
|     private void deleteThisFromFile() { | ||||
|         new File(getMetaFilename()).delete(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the path of the meta file | ||||
|      * run a method in a new thread | ||||
|      * | ||||
|      * @return the path to the meta file | ||||
|      * @param id  id of new thread (used for debugging only) | ||||
|      * @param who the object whose {@code run} method is invoked when this thread is started | ||||
|      * @return the created thread | ||||
|      */ | ||||
|     private String getMetaFilename() { | ||||
|         return location + "/" + name + ".giga"; | ||||
|     private Thread runAsync(int id, Runnable who) { | ||||
|         // known thread ids: | ||||
|         //   -2:     state saving by  notifyProgress()  method | ||||
|         //   -1:     wait for saving the state by  pause()  method | ||||
|         //    0:     initializer | ||||
|         //  >=1:     any download thread | ||||
|  | ||||
|         Thread thread = new Thread(who); | ||||
|         if (DEBUG) { | ||||
|             thread.setName(String.format("[%s]   id = %s   filename = %s", TAG, id, name)); | ||||
|         } | ||||
|         thread.start(); | ||||
|  | ||||
|         return thread; | ||||
|     } | ||||
|  | ||||
|     public File getDownloadedFile() { | ||||
|         return new File(location, name); | ||||
|     static class HttpError extends Exception { | ||||
|         int statusCode; | ||||
|  | ||||
|         HttpError(int statusCode) { | ||||
|             this.statusCode = statusCode; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public String getMessage() { | ||||
|             return "Http status code" + String.valueOf(statusCode); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -2,9 +2,12 @@ package us.shandian.giga.get; | ||||
|  | ||||
| import android.util.Log; | ||||
|  | ||||
| import java.io.BufferedInputStream; | ||||
| import java.io.FileNotFoundException; | ||||
| import java.io.RandomAccessFile; | ||||
| import java.net.HttpURLConnection; | ||||
| import java.net.URL; | ||||
| import java.nio.channels.ClosedByInterruptException; | ||||
|  | ||||
| import static org.schabi.newpipe.BuildConfig.DEBUG; | ||||
|  | ||||
| @@ -18,7 +21,7 @@ public class DownloadRunnable implements Runnable { | ||||
|     private final DownloadMission mMission; | ||||
|     private final int mId; | ||||
|  | ||||
|     public DownloadRunnable(DownloadMission mission, int id) { | ||||
|     DownloadRunnable(DownloadMission mission, int id) { | ||||
|         if (mission == null) throw new NullPointerException("mission is null"); | ||||
|         mMission = mission; | ||||
|         mId = id; | ||||
| @@ -27,14 +30,25 @@ public class DownloadRunnable implements Runnable { | ||||
|     @Override | ||||
|     public void run() { | ||||
|         boolean retry = mMission.recovered; | ||||
|         long position = mMission.getPosition(mId); | ||||
|         long blockPosition = mMission.getBlockPosition(mId); | ||||
|         int retryCount = 0; | ||||
|  | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, mId + ":default pos " + position); | ||||
|             Log.d(TAG, mId + ":default pos " + blockPosition); | ||||
|             Log.d(TAG, mId + ":recovered: " + mMission.recovered); | ||||
|         } | ||||
|  | ||||
|         while (mMission.errCode == -1 && mMission.running && position < mMission.blocks) { | ||||
|         BufferedInputStream ipt = null; | ||||
|         RandomAccessFile f; | ||||
|  | ||||
|         try { | ||||
|             f = new RandomAccessFile(mMission.getDownloadedFile(), "rw"); | ||||
|         } catch (FileNotFoundException e) { | ||||
|             mMission.notifyError(e);// this never should happen | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         while (mMission.errCode == DownloadMission.ERROR_NOTHING && mMission.running && blockPosition < mMission.blocks) { | ||||
|  | ||||
|             if (Thread.currentThread().isInterrupted()) { | ||||
|                 mMission.pause(); | ||||
| @@ -42,57 +56,47 @@ public class DownloadRunnable implements Runnable { | ||||
|             } | ||||
|  | ||||
|             if (DEBUG && retry) { | ||||
|                 Log.d(TAG, mId + ":retry is true. Resuming at " + position); | ||||
|                 Log.d(TAG, mId + ":retry is true. Resuming at " + blockPosition); | ||||
|             } | ||||
|  | ||||
|             // Wait for an unblocked position | ||||
|             while (!retry && position < mMission.blocks && mMission.isBlockPreserved(position)) { | ||||
|             while (!retry && blockPosition < mMission.blocks && mMission.isBlockPreserved(blockPosition)) { | ||||
|  | ||||
|                 if (DEBUG) { | ||||
|                     Log.d(TAG, mId + ":position " + position + " preserved, passing"); | ||||
|                     Log.d(TAG, mId + ":position " + blockPosition + " preserved, passing"); | ||||
|                 } | ||||
|  | ||||
|                 position++; | ||||
|                 blockPosition++; | ||||
|             } | ||||
|  | ||||
|             retry = false; | ||||
|  | ||||
|             if (position >= mMission.blocks) { | ||||
|             if (blockPosition >= mMission.blocks) { | ||||
|                 break; | ||||
|             } | ||||
|  | ||||
|             if (DEBUG) { | ||||
|                 Log.d(TAG, mId + ":preserving position " + position); | ||||
|                 Log.d(TAG, mId + ":preserving position " + blockPosition); | ||||
|             } | ||||
|  | ||||
|             mMission.preserveBlock(position); | ||||
|             mMission.setPosition(mId, position); | ||||
|             mMission.preserveBlock(blockPosition); | ||||
|             mMission.setBlockPosition(mId, blockPosition); | ||||
|  | ||||
|             long start = position * DownloadManager.BLOCK_SIZE; | ||||
|             long end = start + DownloadManager.BLOCK_SIZE - 1; | ||||
|             long start = (blockPosition * DownloadMission.BLOCK_SIZE) + mMission.getBlockBytePosition(mId); | ||||
|             long end = start + DownloadMission.BLOCK_SIZE - 1; | ||||
|  | ||||
|             if (end >= mMission.length) { | ||||
|                 end = mMission.length - 1; | ||||
|             } | ||||
|  | ||||
|             HttpURLConnection conn = null; | ||||
|  | ||||
|             int total = 0; | ||||
|  | ||||
|             try { | ||||
|                 URL url = new URL(mMission.url); | ||||
|                 conn = (HttpURLConnection) url.openConnection(); | ||||
|                 conn.setRequestProperty("Range", "bytes=" + start + "-" + end); | ||||
|                 HttpURLConnection conn = mMission.openConnection(mId, start, end); | ||||
|  | ||||
|                 if (DEBUG) { | ||||
|                     Log.d(TAG, mId + ":" + conn.getRequestProperty("Range")); | ||||
|                     Log.d(TAG, mId + ":Content-Length=" + conn.getContentLength() + " Code:" + conn.getResponseCode()); | ||||
|                 } | ||||
|  | ||||
|                 // A server may be ignoring the range request | ||||
|                 // The server may be ignoring the range request | ||||
|                 if (conn.getResponseCode() != 206) { | ||||
|                     mMission.errCode = DownloadMission.ERROR_SERVER_UNSUPPORTED; | ||||
|                     notifyError(); | ||||
|                     mMission.notifyError(new DownloadMission.HttpError(conn.getResponseCode())); | ||||
|  | ||||
|                     if (DEBUG) { | ||||
|                         Log.e(TAG, mId + ":Unsupported " + conn.getResponseCode()); | ||||
| @@ -101,76 +105,67 @@ public class DownloadRunnable implements Runnable { | ||||
|                     break; | ||||
|                 } | ||||
|  | ||||
|                 RandomAccessFile f = new RandomAccessFile(mMission.location + "/" + mMission.name, "rw"); | ||||
|                 f.seek(start); | ||||
|                 java.io.InputStream ipt = conn.getInputStream(); | ||||
|                 byte[] buf = new byte[64*1024]; | ||||
|                 f.seek(mMission.offsets[mMission.current] + start); | ||||
|  | ||||
|                 while (start < end && mMission.running) { | ||||
|                     int len = ipt.read(buf, 0, buf.length); | ||||
|                 ipt = new BufferedInputStream(conn.getInputStream()); | ||||
|                 byte[] buf = new byte[DownloadMission.BUFFER_SIZE]; | ||||
|                 int len; | ||||
|  | ||||
|                     if (len == -1) { | ||||
|                         break; | ||||
|                     } else { | ||||
|                 while (start < end && mMission.running && (len = ipt.read(buf, 0, buf.length)) != -1) { | ||||
|                     f.write(buf, 0, len); | ||||
|                     start += len; | ||||
|                     total += len; | ||||
|                         f.write(buf, 0, len); | ||||
|                         notifyProgress(len); | ||||
|                     } | ||||
|                     mMission.notifyProgress(len); | ||||
|                 } | ||||
|  | ||||
|                 if (DEBUG && mMission.running) { | ||||
|                     Log.d(TAG, mId + ":position " + position + " finished, total length " + total); | ||||
|                     Log.d(TAG, mId + ":position " + blockPosition + " finished, total length " + total); | ||||
|                 } | ||||
|  | ||||
|                 f.close(); | ||||
|                 ipt.close(); | ||||
|  | ||||
|                 // TODO We should save progress for each thread | ||||
|                 // if the download is paused, save progress for this thread | ||||
|                 if (!mMission.running) { | ||||
|                     mMission.setThreadBytePosition(mId, total); | ||||
|                     break; | ||||
|                 } | ||||
|             } catch (Exception e) { | ||||
|                 // TODO Retry count limit & notify error | ||||
|                 retry = true; | ||||
|                 mMission.setThreadBytePosition(mId, total); | ||||
|  | ||||
|                 notifyProgress(-total); | ||||
|                 if (e instanceof ClosedByInterruptException) break; | ||||
|  | ||||
|                 if (DEBUG) { | ||||
|                     Log.d(TAG, mId + ":position " + position + " retrying", e); | ||||
|                 } | ||||
|             } | ||||
|                 if (retryCount++ > mMission.maxRetry) { | ||||
|                     mMission.notifyError(e); | ||||
|                     break; | ||||
|                 } | ||||
|  | ||||
|                 if (DEBUG) { | ||||
|             Log.d(TAG, "thread " + mId + " exited main loop"); | ||||
|                     Log.d(TAG, mId + ":position " + blockPosition + " retrying due exception", e); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (mMission.errCode == -1 && mMission.running) { | ||||
|         try { | ||||
|             f.close(); | ||||
|         } catch (Exception err) { | ||||
|             // ¿ejected media storage? ¿file deleted? ¿storage ran out of space? | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             if (ipt != null) ipt.close(); | ||||
|         } catch (Exception err) { | ||||
|             // nothing to do | ||||
|         } | ||||
|  | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "thread " + mId + " exited from main download loop"); | ||||
|         } | ||||
|         if (mMission.errCode == DownloadMission.ERROR_NOTHING && mMission.running) { | ||||
|             if (DEBUG) { | ||||
|                 Log.d(TAG, "no error has happened, notifying"); | ||||
|             } | ||||
|             notifyFinished(); | ||||
|             mMission.notifyFinished(); | ||||
|         } | ||||
|  | ||||
|         if (DEBUG && !mMission.running) { | ||||
|             Log.d(TAG, "The mission has been paused. Passing."); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void notifyProgress(final long len) { | ||||
|         synchronized (mMission) { | ||||
|             mMission.notifyProgress(len); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void notifyError() { | ||||
|         synchronized (mMission) { | ||||
|             mMission.notifyError(DownloadMission.ERROR_SERVER_UNSUPPORTED); | ||||
|             mMission.pause(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void notifyFinished() { | ||||
|         synchronized (mMission) { | ||||
|             mMission.notifyFinished(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,74 +1,109 @@ | ||||
| package us.shandian.giga.get; | ||||
|  | ||||
| import android.support.annotation.NonNull; | ||||
| import android.util.Log; | ||||
|  | ||||
| import java.io.BufferedInputStream; | ||||
| import java.io.IOException; | ||||
| import java.io.RandomAccessFile; | ||||
| import java.net.HttpURLConnection; | ||||
| import java.net.URL; | ||||
| import java.nio.channels.ClosedByInterruptException; | ||||
|  | ||||
|  | ||||
| import static org.schabi.newpipe.BuildConfig.DEBUG; | ||||
|  | ||||
| // Single-threaded fallback mode | ||||
| public class DownloadRunnableFallback implements Runnable { | ||||
|     private final DownloadMission mMission; | ||||
|     //private int mId; | ||||
|     private static final String TAG = "DownloadRunnableFallbac"; | ||||
|  | ||||
|     public DownloadRunnableFallback(DownloadMission mission) { | ||||
|         if (mission == null) throw new NullPointerException("mission is null"); | ||||
|         //mId = id; | ||||
|     private final DownloadMission mMission; | ||||
|     private int retryCount = 0; | ||||
|  | ||||
|     private BufferedInputStream ipt; | ||||
|     private RandomAccessFile f; | ||||
|  | ||||
|     DownloadRunnableFallback(@NonNull DownloadMission mission) { | ||||
|         mMission = mission; | ||||
|         ipt = null; | ||||
|         f = null; | ||||
|     } | ||||
|  | ||||
|     private void dispose() { | ||||
|         try { | ||||
|             if (ipt != null) ipt.close(); | ||||
|         } catch (IOException e) { | ||||
|             // nothing to do | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             if (f != null) f.close(); | ||||
|         } catch (IOException e) { | ||||
|             // ¿ejected media storage? ¿file deleted? ¿storage ran out of space? | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void run() { | ||||
|         try { | ||||
|             URL url = new URL(mMission.url); | ||||
|             HttpURLConnection conn = (HttpURLConnection) url.openConnection(); | ||||
|         boolean done; | ||||
|  | ||||
|             if (conn.getResponseCode() != 200 && conn.getResponseCode() != 206) { | ||||
|                 notifyError(DownloadMission.ERROR_SERVER_UNSUPPORTED); | ||||
|             } else { | ||||
|                 RandomAccessFile f = new RandomAccessFile(mMission.location + "/" + mMission.name, "rw"); | ||||
|                 f.seek(0); | ||||
|                 BufferedInputStream ipt = new BufferedInputStream(conn.getInputStream()); | ||||
|                 byte[] buf = new byte[512]; | ||||
|         int start = 0; | ||||
|  | ||||
|         if (!mMission.unknownLength) { | ||||
|             start = mMission.getBlockBytePosition(0); | ||||
|             if (DEBUG && start > 0) { | ||||
|                 Log.i(TAG, "Resuming a single-thread download at " + start); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             int rangeStart = (mMission.unknownLength || start < 1) ? -1 : start; | ||||
|             HttpURLConnection conn = mMission.openConnection(1, rangeStart, -1); | ||||
|  | ||||
|             // secondary check for the file length | ||||
|             if (!mMission.unknownLength) mMission.unknownLength = conn.getContentLength() == -1; | ||||
|  | ||||
|             f = new RandomAccessFile(mMission.getDownloadedFile(), "rw"); | ||||
|             f.seek(mMission.offsets[mMission.current] + start); | ||||
|  | ||||
|             ipt = new BufferedInputStream(conn.getInputStream()); | ||||
|  | ||||
|             byte[] buf = new byte[DownloadMission.BUFFER_SIZE]; | ||||
|             int len = 0; | ||||
|  | ||||
|                 while ((len = ipt.read(buf, 0, 512)) != -1 && mMission.running) { | ||||
|             while (mMission.running && (len = ipt.read(buf, 0, buf.length)) != -1) { | ||||
|                 f.write(buf, 0, len); | ||||
|                     notifyProgress(len); | ||||
|                 start += len; | ||||
|  | ||||
|                     if (Thread.interrupted()) { | ||||
|                         break; | ||||
|                     } | ||||
|  | ||||
|                 } | ||||
|  | ||||
|                 f.close(); | ||||
|                 ipt.close(); | ||||
|             } | ||||
|         } catch (Exception e) { | ||||
|             notifyError(DownloadMission.ERROR_UNKNOWN); | ||||
|         } | ||||
|  | ||||
|         if (mMission.errCode == -1 && mMission.running) { | ||||
|             notifyFinished(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void notifyProgress(final long len) { | ||||
|         synchronized (mMission) { | ||||
|                 mMission.notifyProgress(len); | ||||
|         } | ||||
|  | ||||
|                 if (Thread.interrupted()) break; | ||||
|             } | ||||
|  | ||||
|     private void notifyError(final int err) { | ||||
|         synchronized (mMission) { | ||||
|             mMission.notifyError(err); | ||||
|             mMission.pause(); | ||||
|         } | ||||
|             // if thread goes interrupted check if the last part is written. This avoid re-download the whole file | ||||
|             done = len == -1; | ||||
|         } catch (Exception e) { | ||||
|             dispose(); | ||||
|  | ||||
|             // save position | ||||
|             mMission.setThreadBytePosition(0, start); | ||||
|  | ||||
|             if (e instanceof ClosedByInterruptException) return; | ||||
|  | ||||
|             if (retryCount++ > mMission.maxRetry) { | ||||
|                 mMission.notifyError(e); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|     private void notifyFinished() { | ||||
|         synchronized (mMission) { | ||||
|             run();// try again | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         dispose(); | ||||
|  | ||||
|         if (done) { | ||||
|             mMission.notifyFinished(); | ||||
|         } else { | ||||
|             mMission.setThreadBytePosition(0, start); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										16
									
								
								app/src/main/java/us/shandian/giga/get/FinishedMission.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								app/src/main/java/us/shandian/giga/get/FinishedMission.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| package us.shandian.giga.get; | ||||
|  | ||||
| public class FinishedMission extends  Mission { | ||||
|  | ||||
|     public FinishedMission() { | ||||
|     } | ||||
|  | ||||
|     public FinishedMission(DownloadMission mission) { | ||||
|         source = mission.source; | ||||
|         length = mission.length;// ¿or mission.done? | ||||
|         timestamp = mission.timestamp; | ||||
|         name = mission.name; | ||||
|         location = mission.location; | ||||
|         kind = mission.kind; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										66
									
								
								app/src/main/java/us/shandian/giga/get/Mission.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								app/src/main/java/us/shandian/giga/get/Mission.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| package us.shandian.giga.get; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.Serializable; | ||||
| import java.text.SimpleDateFormat; | ||||
| import java.util.Calendar; | ||||
|  | ||||
| public abstract class Mission implements Serializable { | ||||
|     private static final long serialVersionUID = 0L;// last bump: 5 october 2018 | ||||
|  | ||||
|     /** | ||||
|      * Source url of the resource | ||||
|      */ | ||||
|     public String source; | ||||
|  | ||||
|     /** | ||||
|      * Length of the current resource | ||||
|      */ | ||||
|     public long length; | ||||
|  | ||||
|     /** | ||||
|      * creation timestamp (and maybe unique identifier) | ||||
|      */ | ||||
|     public long timestamp; | ||||
|  | ||||
|     /** | ||||
|      * The filename | ||||
|      */ | ||||
|     public String name; | ||||
|  | ||||
|     /** | ||||
|      * The directory to store the download | ||||
|      */ | ||||
|     public String location; | ||||
|  | ||||
|     /** | ||||
|      * pre-defined content type | ||||
|      */ | ||||
|     public char kind; | ||||
|  | ||||
|     /** | ||||
|      * get the target file on the storage | ||||
|      * | ||||
|      * @return File object | ||||
|      */ | ||||
|     public File getDownloadedFile() { | ||||
|         return new File(location, name); | ||||
|     } | ||||
|  | ||||
|     public boolean delete() { | ||||
|         deleted = true; | ||||
|         return getDownloadedFile().delete(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Indicate if this mission is deleted whatever is stored | ||||
|      */ | ||||
|     public transient boolean deleted = false; | ||||
|  | ||||
|     @Override | ||||
|     public String toString() { | ||||
|         Calendar calendar = Calendar.getInstance(); | ||||
|         calendar.setTimeInMillis(timestamp); | ||||
|         return "[" + calendar.getTime().toString() + "] " + location + File.separator + name; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,73 @@ | ||||
| package us.shandian.giga.get.sqlite; | ||||
|  | ||||
| import android.content.ContentValues; | ||||
| import android.content.Context; | ||||
| import android.database.Cursor; | ||||
| import android.database.sqlite.SQLiteDatabase; | ||||
| import android.util.Log; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
|  | ||||
| import us.shandian.giga.get.DownloadMission; | ||||
| import us.shandian.giga.get.FinishedMission; | ||||
| import us.shandian.giga.get.Mission; | ||||
|  | ||||
| import static us.shandian.giga.get.sqlite.DownloadMissionHelper.KEY_LOCATION; | ||||
| import static us.shandian.giga.get.sqlite.DownloadMissionHelper.KEY_NAME; | ||||
| import static us.shandian.giga.get.sqlite.DownloadMissionHelper.MISSIONS_TABLE_NAME; | ||||
|  | ||||
| public class DownloadDataSource { | ||||
|  | ||||
|     private static final String TAG = "DownloadDataSource"; | ||||
|     private final DownloadMissionHelper downloadMissionHelper; | ||||
|  | ||||
|     public DownloadDataSource(Context context) { | ||||
|         downloadMissionHelper = new DownloadMissionHelper(context); | ||||
|     } | ||||
|  | ||||
|     public ArrayList<FinishedMission> loadFinishedMissions() { | ||||
|         SQLiteDatabase database = downloadMissionHelper.getReadableDatabase(); | ||||
|         Cursor cursor = database.query(MISSIONS_TABLE_NAME, null, null, | ||||
|                 null, null, null, DownloadMissionHelper.KEY_TIMESTAMP); | ||||
|  | ||||
|         int count = cursor.getCount(); | ||||
|         if (count == 0) return new ArrayList<>(1); | ||||
|  | ||||
|         ArrayList<FinishedMission> result = new ArrayList<>(count); | ||||
|         while (cursor.moveToNext()) { | ||||
|             result.add(DownloadMissionHelper.getMissionFromCursor(cursor)); | ||||
|         } | ||||
|  | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|     public void addMission(DownloadMission downloadMission) { | ||||
|         if (downloadMission == null) throw new NullPointerException("downloadMission is null"); | ||||
|         SQLiteDatabase database = downloadMissionHelper.getWritableDatabase(); | ||||
|         ContentValues values = DownloadMissionHelper.getValuesOfMission(downloadMission); | ||||
|         database.insert(MISSIONS_TABLE_NAME, null, values); | ||||
|     } | ||||
|  | ||||
|     public void deleteMission(Mission downloadMission) { | ||||
|         if (downloadMission == null) throw new NullPointerException("downloadMission is null"); | ||||
|         SQLiteDatabase database = downloadMissionHelper.getWritableDatabase(); | ||||
|         database.delete(MISSIONS_TABLE_NAME, | ||||
|                 KEY_LOCATION + " = ? AND " + | ||||
|                         KEY_NAME + " = ?", | ||||
|                 new String[]{downloadMission.location, downloadMission.name}); | ||||
|     } | ||||
|  | ||||
|     public void updateMission(DownloadMission downloadMission) { | ||||
|         if (downloadMission == null) throw new NullPointerException("downloadMission is null"); | ||||
|         SQLiteDatabase database = downloadMissionHelper.getWritableDatabase(); | ||||
|         ContentValues values = DownloadMissionHelper.getValuesOfMission(downloadMission); | ||||
|         String whereClause = KEY_LOCATION + " = ? AND " + | ||||
|                 KEY_NAME + " = ?"; | ||||
|         int rowsAffected = database.update(MISSIONS_TABLE_NAME, values, | ||||
|                 whereClause, new String[]{downloadMission.location, downloadMission.name}); | ||||
|         if (rowsAffected != 1) { | ||||
|             Log.e(TAG, "Expected 1 row to be affected by update but got " + rowsAffected); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -7,19 +7,19 @@ import android.database.sqlite.SQLiteDatabase; | ||||
| import android.database.sqlite.SQLiteOpenHelper; | ||||
| 
 | ||||
| import us.shandian.giga.get.DownloadMission; | ||||
| import us.shandian.giga.get.FinishedMission; | ||||
| 
 | ||||
| /** | ||||
|  * SqliteHelper to store {@link us.shandian.giga.get.DownloadMission} | ||||
|  * SQLiteHelper to store finished {@link us.shandian.giga.get.DownloadMission}'s | ||||
|  */ | ||||
| public class DownloadMissionSQLiteHelper extends SQLiteOpenHelper { | ||||
| 
 | ||||
| 
 | ||||
| public class DownloadMissionHelper extends SQLiteOpenHelper { | ||||
|     private final String TAG = "DownloadMissionHelper"; | ||||
| 
 | ||||
|     // TODO: use NewPipeSQLiteHelper ('s constants) when playlist branch is merged (?) | ||||
|     private static final String DATABASE_NAME = "downloads.db"; | ||||
| 
 | ||||
|     private static final int DATABASE_VERSION = 2; | ||||
|     private static final int DATABASE_VERSION = 3; | ||||
| 
 | ||||
|     /** | ||||
|      * The table name of download missions | ||||
|      */ | ||||
| @@ -30,9 +30,9 @@ public class DownloadMissionSQLiteHelper extends SQLiteOpenHelper { | ||||
|      */ | ||||
|     static final String KEY_LOCATION = "location"; | ||||
|     /** | ||||
|      * The key to the url of a mission | ||||
|      * The key to the urls of a mission | ||||
|      */ | ||||
|     static final String KEY_URL = "url"; | ||||
|     static final String KEY_SOURCE_URL = "url"; | ||||
|     /** | ||||
|      * The key to the name of a mission | ||||
|      */ | ||||
| @@ -45,6 +45,8 @@ public class DownloadMissionSQLiteHelper extends SQLiteOpenHelper { | ||||
| 
 | ||||
|     static final String KEY_TIMESTAMP = "timestamp"; | ||||
| 
 | ||||
|     static  final String KEY_KIND = "kind"; | ||||
| 
 | ||||
|     /** | ||||
|      * The statement to create the table | ||||
|      */ | ||||
| @@ -52,16 +54,28 @@ public class DownloadMissionSQLiteHelper extends SQLiteOpenHelper { | ||||
|             "CREATE TABLE " + MISSIONS_TABLE_NAME + " (" + | ||||
|                     KEY_LOCATION + " TEXT NOT NULL, " + | ||||
|                     KEY_NAME + " TEXT NOT NULL, " + | ||||
|                     KEY_URL + " TEXT NOT NULL, " + | ||||
|                     KEY_SOURCE_URL + " TEXT NOT NULL, " + | ||||
|                     KEY_DONE + " INTEGER NOT NULL, " + | ||||
|                     KEY_TIMESTAMP + " INTEGER NOT NULL, " + | ||||
|                     KEY_KIND + " TEXT NOT NULL, " + | ||||
|                     " UNIQUE(" + KEY_LOCATION + ", " + KEY_NAME + "));"; | ||||
| 
 | ||||
| 
 | ||||
|     DownloadMissionSQLiteHelper(Context context) { | ||||
|     public DownloadMissionHelper(Context context) { | ||||
|         super(context, DATABASE_NAME, null, DATABASE_VERSION); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCreate(SQLiteDatabase db) { | ||||
|         db.execSQL(MISSIONS_CREATE_TABLE); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { | ||||
|         if (oldVersion == 2) { | ||||
|             db.execSQL("ALTER TABLE " + MISSIONS_TABLE_NAME + " ADD COLUMN " + KEY_KIND + " TEXT;"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns all values of the download mission as ContentValues. | ||||
|      * | ||||
| @@ -70,34 +84,29 @@ public class DownloadMissionSQLiteHelper extends SQLiteOpenHelper { | ||||
|      */ | ||||
|     public static ContentValues getValuesOfMission(DownloadMission downloadMission) { | ||||
|         ContentValues values = new ContentValues(); | ||||
|         values.put(KEY_URL, downloadMission.url); | ||||
|         values.put(KEY_SOURCE_URL, downloadMission.source); | ||||
|         values.put(KEY_LOCATION, downloadMission.location); | ||||
|         values.put(KEY_NAME, downloadMission.name); | ||||
|         values.put(KEY_DONE, downloadMission.done); | ||||
|         values.put(KEY_TIMESTAMP, downloadMission.timestamp); | ||||
|         values.put(KEY_KIND, String.valueOf(downloadMission.kind)); | ||||
|         return values; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCreate(SQLiteDatabase db) { | ||||
|         db.execSQL(MISSIONS_CREATE_TABLE); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { | ||||
|         // Currently nothing to do | ||||
|     } | ||||
| 
 | ||||
|     public static DownloadMission getMissionFromCursor(Cursor cursor) { | ||||
|     public static FinishedMission getMissionFromCursor(Cursor cursor) { | ||||
|         if (cursor == null) throw new NullPointerException("cursor is null"); | ||||
|         int pos; | ||||
|         String name = cursor.getString(cursor.getColumnIndexOrThrow(KEY_NAME)); | ||||
|         String location = cursor.getString(cursor.getColumnIndexOrThrow(KEY_LOCATION)); | ||||
|         String url = cursor.getString(cursor.getColumnIndexOrThrow(KEY_URL)); | ||||
|         DownloadMission mission = new DownloadMission(name, url, location); | ||||
|         mission.done = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE)); | ||||
| 
 | ||||
|         String kind = cursor.getString(cursor.getColumnIndex(KEY_KIND)); | ||||
|         if (kind == null || kind.isEmpty()) kind = "?"; | ||||
| 
 | ||||
|         FinishedMission mission = new FinishedMission(); | ||||
|         mission.name = cursor.getString(cursor.getColumnIndexOrThrow(KEY_NAME)); | ||||
|         mission.location = cursor.getString(cursor.getColumnIndexOrThrow(KEY_LOCATION)); | ||||
|         mission.source = cursor.getString(cursor.getColumnIndexOrThrow(KEY_SOURCE_URL));; | ||||
|         mission.length = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE)); | ||||
|         mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP)); | ||||
|         mission.finished = true; | ||||
|         mission.kind = kind.charAt(0); | ||||
| 
 | ||||
|         return mission; | ||||
|     } | ||||
| } | ||||
| @@ -1,79 +0,0 @@ | ||||
| package us.shandian.giga.get.sqlite; | ||||
|  | ||||
| import android.content.ContentValues; | ||||
| import android.content.Context; | ||||
| import android.database.Cursor; | ||||
| import android.database.sqlite.SQLiteDatabase; | ||||
| import android.util.Log; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
|  | ||||
| import us.shandian.giga.get.DownloadDataSource; | ||||
| import us.shandian.giga.get.DownloadMission; | ||||
|  | ||||
| import static us.shandian.giga.get.sqlite.DownloadMissionSQLiteHelper.KEY_LOCATION; | ||||
| import static us.shandian.giga.get.sqlite.DownloadMissionSQLiteHelper.KEY_NAME; | ||||
| import static us.shandian.giga.get.sqlite.DownloadMissionSQLiteHelper.MISSIONS_TABLE_NAME; | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * Non-thread-safe implementation of {@link DownloadDataSource} | ||||
|  */ | ||||
| public class SQLiteDownloadDataSource implements DownloadDataSource { | ||||
|  | ||||
|     private static final String TAG = "DownloadDataSourceImpl"; | ||||
|     private final DownloadMissionSQLiteHelper downloadMissionSQLiteHelper; | ||||
|  | ||||
|     public SQLiteDownloadDataSource(Context context) { | ||||
|         downloadMissionSQLiteHelper = new DownloadMissionSQLiteHelper(context); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public List<DownloadMission> loadMissions() { | ||||
|         ArrayList<DownloadMission> result; | ||||
|         SQLiteDatabase database = downloadMissionSQLiteHelper.getReadableDatabase(); | ||||
|         Cursor cursor = database.query(MISSIONS_TABLE_NAME, null, null, | ||||
|                 null, null, null, DownloadMissionSQLiteHelper.KEY_TIMESTAMP); | ||||
|  | ||||
|         int count = cursor.getCount(); | ||||
|         if (count == 0) return new ArrayList<>(); | ||||
|         result = new ArrayList<>(count); | ||||
|         while (cursor.moveToNext()) { | ||||
|             result.add(DownloadMissionSQLiteHelper.getMissionFromCursor(cursor)); | ||||
|         } | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void addMission(DownloadMission downloadMission) { | ||||
|         if (downloadMission == null) throw new NullPointerException("downloadMission is null"); | ||||
|         SQLiteDatabase database = downloadMissionSQLiteHelper.getWritableDatabase(); | ||||
|         ContentValues values = DownloadMissionSQLiteHelper.getValuesOfMission(downloadMission); | ||||
|         database.insert(MISSIONS_TABLE_NAME, null, values); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void updateMission(DownloadMission downloadMission) { | ||||
|         if (downloadMission == null) throw new NullPointerException("downloadMission is null"); | ||||
|         SQLiteDatabase database = downloadMissionSQLiteHelper.getWritableDatabase(); | ||||
|         ContentValues values = DownloadMissionSQLiteHelper.getValuesOfMission(downloadMission); | ||||
|         String whereClause = KEY_LOCATION + " = ? AND " + | ||||
|                 KEY_NAME + " = ?"; | ||||
|         int rowsAffected = database.update(MISSIONS_TABLE_NAME, values, | ||||
|                 whereClause, new String[]{downloadMission.location, downloadMission.name}); | ||||
|         if (rowsAffected != 1) { | ||||
|             Log.e(TAG, "Expected 1 row to be affected by update but got " + rowsAffected); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void deleteMission(DownloadMission downloadMission) { | ||||
|         if (downloadMission == null) throw new NullPointerException("downloadMission is null"); | ||||
|         SQLiteDatabase database = downloadMissionSQLiteHelper.getWritableDatabase(); | ||||
|         database.delete(MISSIONS_TABLE_NAME, | ||||
|                 KEY_LOCATION + " = ? AND " + | ||||
|                         KEY_NAME + " = ?", | ||||
|                 new String[]{downloadMission.location, downloadMission.name}); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,31 @@ | ||||
| package us.shandian.giga.postprocessing; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.utils.Mp4DashWriter; | ||||
| import org.schabi.newpipe.extractor.utils.io.SharpStream; | ||||
|  | ||||
| import java.io.IOException; | ||||
|  | ||||
| import us.shandian.giga.get.DownloadMission; | ||||
|  | ||||
| /** | ||||
|  * @author kapodamy | ||||
|  */ | ||||
| class Mp4DashMuxer extends Postprocessing { | ||||
|  | ||||
|     Mp4DashMuxer(DownloadMission mission) { | ||||
|         super(mission); | ||||
|         recommendedReserve = 2048 * 1024;// 2 MiB | ||||
|         worksOnSameFile = true; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     int process(SharpStream out, SharpStream... sources) throws IOException { | ||||
|         Mp4DashWriter muxer = new Mp4DashWriter(sources); | ||||
|         muxer.parseSources(); | ||||
|         muxer.selectTracks(0, 0); | ||||
|         muxer.build(out); | ||||
|  | ||||
|         return OK_RESULT; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,149 @@ | ||||
| package us.shandian.giga.postprocessing; | ||||
|  | ||||
| import android.os.Message; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.utils.io.SharpStream; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
|  | ||||
| import us.shandian.giga.get.DownloadMission; | ||||
| import us.shandian.giga.postprocessing.io.ChunkFileInputStream; | ||||
| import us.shandian.giga.postprocessing.io.CircularFile; | ||||
| import us.shandian.giga.service.DownloadManagerService; | ||||
|  | ||||
| public abstract class Postprocessing { | ||||
|  | ||||
|     static final byte OK_RESULT = DownloadMission.ERROR_NOTHING; | ||||
|  | ||||
|     public static final String ALGORITHM_TTML_CONVERTER = "ttml"; | ||||
|     public static final String ALGORITHM_MP4_DASH_MUXER = "mp4D"; | ||||
|     public static final String ALGORITHM_WEBM_MUXER = "webm"; | ||||
|     private static final String ALGORITHM_TEST_ALGO = "test"; | ||||
|  | ||||
|     public static Postprocessing getAlgorithm(String algorithmName, DownloadMission mission) { | ||||
|         if (null == algorithmName) { | ||||
|             throw new NullPointerException("algorithmName"); | ||||
|         } else switch (algorithmName) { | ||||
|             case ALGORITHM_TTML_CONVERTER: | ||||
|                 return new TttmlConverter(mission); | ||||
|             case ALGORITHM_MP4_DASH_MUXER: | ||||
|                 return new Mp4DashMuxer(mission); | ||||
|             case ALGORITHM_WEBM_MUXER: | ||||
|                 return new WebMMuxer(mission); | ||||
|             case ALGORITHM_TEST_ALGO: | ||||
|                 return new TestAlgo(mission); | ||||
|             /*case "example-algorithm": | ||||
|             return new ExampleAlgorithm(mission);*/ | ||||
|             default: | ||||
|                 throw new RuntimeException("Unimplemented post-processing algorithm: " + algorithmName); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a boolean value that indicate if the given algorithm work on the same | ||||
|      * file | ||||
|      */ | ||||
|     public boolean worksOnSameFile; | ||||
|  | ||||
|     /** | ||||
|      * Get the recommended space to reserve for the given algorithm. The amount | ||||
|      * is in bytes | ||||
|      */ | ||||
|     public int recommendedReserve; | ||||
|  | ||||
|     protected DownloadMission mission; | ||||
|  | ||||
|     Postprocessing(DownloadMission mission) { | ||||
|         this.mission = mission; | ||||
|     } | ||||
|  | ||||
|     public void run() throws IOException { | ||||
|         File file = mission.getDownloadedFile(); | ||||
|         CircularFile out = null; | ||||
|         ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length]; | ||||
|  | ||||
|         try { | ||||
|             int i = 0; | ||||
|             for (; i < sources.length - 1; i++) { | ||||
|                 sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.offsets[i + 1], "rw"); | ||||
|             } | ||||
|             sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.getDownloadedFile().length(), "rw"); | ||||
|  | ||||
|             int[] idx = {0}; | ||||
|             CircularFile.OffsetChecker checker = () -> { | ||||
|                 while (idx[0] < sources.length) { | ||||
|                     /* | ||||
|                      * WARNING: never use rewind() in any chunk after any writing (especially on first chunks) | ||||
|                      *          or the CircularFile can lead to unexpected results | ||||
|                      */ | ||||
|                     if (sources[idx[0]].isDisposed() || sources[idx[0]].available() < 1) { | ||||
|                         idx[0]++; | ||||
|                         continue;// the selected source is not used anymore | ||||
|                     } | ||||
|  | ||||
|                     return sources[idx[0]].getFilePointer() - 1; | ||||
|                 } | ||||
|  | ||||
|                 return -1; | ||||
|             }; | ||||
|  | ||||
|             out = new CircularFile(file, 0, this::progressReport, checker); | ||||
|  | ||||
|             mission.done = 0; | ||||
|             int result = process(out, sources); | ||||
|  | ||||
|             if (result == OK_RESULT) { | ||||
|                 long finalLength = out.finalizeFile(); | ||||
|                 mission.done = finalLength; | ||||
|                 mission.length = finalLength; | ||||
|             } else { | ||||
|                 mission.errCode = DownloadMission.ERROR_UNKNOWN_EXCEPTION; | ||||
|                 mission.errObject = new RuntimeException("post-processing algorithm returned " + result); | ||||
|             } | ||||
|  | ||||
|             if (result != OK_RESULT && worksOnSameFile) { | ||||
|                 //noinspection ResultOfMethodCallIgnored | ||||
|                 new File(mission.location, mission.name).delete(); | ||||
|             } | ||||
|         } finally { | ||||
|             for (SharpStream source : sources) { | ||||
|                 if (source != null && !source.isDisposed()) { | ||||
|                     source.dispose(); | ||||
|                 } | ||||
|             } | ||||
|             if (out != null) { | ||||
|                 out.dispose(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Abstract method to execute the pos-processing algorithm | ||||
|      * | ||||
|      * @param out     output stream | ||||
|      * @param sources files to be processed | ||||
|      * @return a error code, 0 means the operation was successful | ||||
|      * @throws IOException if an I/O error occurs. | ||||
|      */ | ||||
|     abstract int process(SharpStream out, SharpStream... sources) throws IOException; | ||||
|  | ||||
|     String getArgumentAt(int index, String defaultValue) { | ||||
|         if (mission.postprocessingArgs == null || index >= mission.postprocessingArgs.length) { | ||||
|             return defaultValue; | ||||
|         } | ||||
|  | ||||
|         return mission.postprocessingArgs[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); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,54 @@ | ||||
| package us.shandian.giga.postprocessing; | ||||
|  | ||||
| import android.util.Log; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.utils.io.SharpStream; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.util.Random; | ||||
|  | ||||
| import us.shandian.giga.get.DownloadMission; | ||||
|  | ||||
| /** | ||||
|  * Algorithm for testing proposes | ||||
|  */ | ||||
| class TestAlgo extends Postprocessing { | ||||
|  | ||||
|     public TestAlgo(DownloadMission mission) { | ||||
|         super(mission); | ||||
|  | ||||
|         worksOnSameFile = true; | ||||
|         recommendedReserve = 4096 * 1024;// 4 KiB | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     int process(SharpStream out, SharpStream... sources) throws IOException { | ||||
|  | ||||
|         int written = 0; | ||||
|         int size = 5 * 1024 * 1024;// 5 MiB | ||||
|         byte[] buffer = new byte[8 * 1024];//8 KiB | ||||
|         mission.length = size; | ||||
|  | ||||
|         Random rnd = new Random(); | ||||
|  | ||||
|         // only write random data | ||||
|         sources[0].dispose(); | ||||
|  | ||||
|         while (written < size) { | ||||
|             rnd.nextBytes(buffer); | ||||
|  | ||||
|             int read = Math.min(buffer.length, size - written); | ||||
|             out.write(buffer, 0, read); | ||||
|  | ||||
|             try { | ||||
|                 Thread.sleep((int) (Math.random() * 10)); | ||||
|             } catch (InterruptedException e) { | ||||
|                 return -1; | ||||
|             } | ||||
|  | ||||
|             written += read; | ||||
|         } | ||||
|  | ||||
|         return Postprocessing.OK_RESULT; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,49 @@ | ||||
| package us.shandian.giga.postprocessing; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.utils.io.SharpStream; | ||||
| import org.schabi.newpipe.extractor.utils.SubtitleConverter; | ||||
|  | ||||
| import java.io.IOException; | ||||
|  | ||||
| import us.shandian.giga.get.DownloadMission; | ||||
| import us.shandian.giga.postprocessing.io.SharpInputStream; | ||||
| /** | ||||
|  * @author kapodamy | ||||
|  */ | ||||
| class TttmlConverter extends Postprocessing { | ||||
|  | ||||
|     TttmlConverter(DownloadMission mission) { | ||||
|         super(mission); | ||||
|         recommendedReserve = 0;// due how XmlPullParser works, the xml is fully loaded on the ram | ||||
|         worksOnSameFile = true; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     int process(SharpStream out, SharpStream... sources) throws IOException { | ||||
|         // check if the subtitle is already in srt and copy, this should never happen | ||||
|         String format = getArgumentAt(0, null); | ||||
|  | ||||
|         if (format == null || format.equals("ttml")) { | ||||
|             SubtitleConverter ttmlDumper = new SubtitleConverter(); | ||||
|  | ||||
|             int res = ttmlDumper.dumpTTML( | ||||
|                     sources[0], | ||||
|                     out, | ||||
|                     getArgumentAt(1, "true").equals("true"), | ||||
|                     getArgumentAt(2, "true").equals("true") | ||||
|             ); | ||||
|  | ||||
|             return res == 0 ? OK_RESULT : res; | ||||
|         } else if (format.equals("srt")) { | ||||
|             byte[] buffer = new byte[8 * 1024]; | ||||
|             int read; | ||||
|             while ((read = sources[0].read(buffer)) > 0) { | ||||
|                 out.write(buffer, 0, read); | ||||
|             } | ||||
|             return OK_RESULT; | ||||
|         } | ||||
|  | ||||
|         throw new UnsupportedOperationException("Can't convert this subtitle, unimplemented format: " + format); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,44 @@ | ||||
| package us.shandian.giga.postprocessing; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.utils.WebMReader.TrackKind; | ||||
| import org.schabi.newpipe.extractor.utils.WebMReader.WebMTrack; | ||||
| import org.schabi.newpipe.extractor.utils.WebMWriter; | ||||
| import org.schabi.newpipe.extractor.utils.io.SharpStream; | ||||
|  | ||||
| import java.io.IOException; | ||||
|  | ||||
| import us.shandian.giga.get.DownloadMission; | ||||
|  | ||||
| /** | ||||
|  * @author kapodamy | ||||
|  */ | ||||
| class WebMMuxer extends Postprocessing { | ||||
|  | ||||
|     WebMMuxer(DownloadMission mission) { | ||||
|         super(mission); | ||||
|         recommendedReserve = (1024 + 512) * 1024;// 1.50 MiB | ||||
|         worksOnSameFile = true; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     int process(SharpStream out, SharpStream... sources) throws IOException { | ||||
|         WebMWriter muxer = new WebMWriter(sources); | ||||
|         muxer.parseSources(); | ||||
|  | ||||
|         // youtube uses a webm with a fake video track that acts as a "cover image" | ||||
|         WebMTrack[] tracks = muxer.getTracksFromSource(1); | ||||
|         int audioTrackIndex = 0; | ||||
|         for (int i = 0; i < tracks.length; i++) { | ||||
|             if (tracks[i].kind == TrackKind.Audio) { | ||||
|                 audioTrackIndex = i; | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         muxer.selectTracks(0, audioTrackIndex); | ||||
|         muxer.build(out); | ||||
|  | ||||
|         return OK_RESULT; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,153 @@ | ||||
| package us.shandian.giga.postprocessing.io; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.utils.io.SharpStream; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.io.RandomAccessFile; | ||||
|  | ||||
| public class ChunkFileInputStream extends SharpStream { | ||||
|  | ||||
|     private RandomAccessFile source; | ||||
|     private final long offset; | ||||
|     private final long length; | ||||
|     private long position; | ||||
|  | ||||
|     public ChunkFileInputStream(File file, long start, long end, String mode) throws IOException { | ||||
|         source = new RandomAccessFile(file, mode); | ||||
|         offset = start; | ||||
|         length = end - start; | ||||
|         position = 0; | ||||
|  | ||||
|         if (length < 1) { | ||||
|             source.close(); | ||||
|             throw new IOException("The chunk is empty or invalid"); | ||||
|         } | ||||
|         if (source.length() < end) { | ||||
|             try { | ||||
|                 throw new IOException(String.format("invalid file length. expected = %s  found = %s", end, source.length())); | ||||
|             } finally { | ||||
|                 source.close(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         source.seek(offset); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get absolute position on file | ||||
|      * | ||||
|      * @return the position | ||||
|      */ | ||||
|     public long getFilePointer() { | ||||
|         return offset + position; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int read() throws IOException { | ||||
|         if ((position + 1) > length) { | ||||
|             return 0; | ||||
|         } | ||||
|  | ||||
|         int res = source.read(); | ||||
|         if (res >= 0) { | ||||
|             position++; | ||||
|         } | ||||
|  | ||||
|         return res; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     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 { | ||||
|         if ((position + len) > length) { | ||||
|             len = (int) (length - position); | ||||
|         } | ||||
|         if (len == 0) { | ||||
|             return 0; | ||||
|         } | ||||
|  | ||||
|         int res = source.read(b, off, len); | ||||
|         position += res; | ||||
|  | ||||
|         return res; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public long skip(long pos) throws IOException { | ||||
|         pos = Math.min(pos + position, length); | ||||
|  | ||||
|         if (pos == 0) { | ||||
|             return 0; | ||||
|         } | ||||
|  | ||||
|         source.seek(offset + pos); | ||||
|  | ||||
|         long oldPos = position; | ||||
|         position = pos; | ||||
|  | ||||
|         return pos - oldPos; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int available() { | ||||
|         return (int) (length - position); | ||||
|     } | ||||
|  | ||||
|     @SuppressWarnings("EmptyCatchBlock") | ||||
|     @Override | ||||
|     public void dispose() { | ||||
|         try { | ||||
|             source.close(); | ||||
|         } catch (IOException err) { | ||||
|         } finally { | ||||
|             source = null; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean isDisposed() { | ||||
|         return source == null; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void rewind() throws IOException { | ||||
|         position = 0; | ||||
|         source.seek(offset); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean canRewind() { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean canRead() { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean canWrite() { | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void write(byte value) { | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void write(byte[] buffer) { | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void write(byte[] buffer, int offset, int count) { | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void flush() { | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,345 @@ | ||||
| package us.shandian.giga.postprocessing.io; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.utils.io.SharpStream; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.io.RandomAccessFile; | ||||
| import java.util.ArrayList; | ||||
|  | ||||
| public class CircularFile extends SharpStream { | ||||
|  | ||||
|     private final static int AUX_BUFFER_SIZE = 1024 * 1024;// 1 MiB | ||||
|     private final static int AUX2_BUFFER_SIZE = 256 * 1024;// 256 KiB | ||||
|     private final static int QUEUE_BUFFER_SIZE = 8 * 1024;// 8 KiB | ||||
|  | ||||
|     private RandomAccessFile out; | ||||
|     private long position; | ||||
|     private long maxLengthKnown = -1; | ||||
|  | ||||
|     private ArrayList<ManagedBuffer> auxiliaryBuffers; | ||||
|     private OffsetChecker callback; | ||||
|     private ManagedBuffer queue; | ||||
|     private long startOffset; | ||||
|     private ProgressReport onProgress; | ||||
|     private long reportPosition; | ||||
|  | ||||
|     public CircularFile(File file, long offset, ProgressReport progressReport, OffsetChecker checker) throws IOException { | ||||
|         if (checker == null) { | ||||
|             throw new NullPointerException("checker is null"); | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             queue = new ManagedBuffer(QUEUE_BUFFER_SIZE); | ||||
|             out = new RandomAccessFile(file, "rw"); | ||||
|             out.seek(offset); | ||||
|             position = offset; | ||||
|         } catch (IOException err) { | ||||
|             try { | ||||
|                 if (out != null) { | ||||
|                     out.close(); | ||||
|                 } | ||||
|             } catch (IOException e) { | ||||
|                 // nothing to do | ||||
|             } | ||||
|             throw err; | ||||
|         } | ||||
|  | ||||
|         auxiliaryBuffers = new ArrayList<>(1); | ||||
|         callback = checker; | ||||
|         startOffset = offset; | ||||
|         reportPosition = offset; | ||||
|         onProgress = progressReport; | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Close the file without flushing any buffer | ||||
|      */ | ||||
|     @Override | ||||
|     public void dispose() { | ||||
|         try { | ||||
|             auxiliaryBuffers = null; | ||||
|             if (out != null) { | ||||
|                 out.close(); | ||||
|                 out = null; | ||||
|             } | ||||
|         } catch (IOException err) { | ||||
|             // nothing to do | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Flush any buffer and close the output file. Use this method if the | ||||
|      * operation is successful | ||||
|      * | ||||
|      * @return the final length of the file | ||||
|      * @throws IOException if an I/O error occurs | ||||
|      */ | ||||
|     public long finalizeFile() throws IOException { | ||||
|         flushEverything(); | ||||
|  | ||||
|         if (maxLengthKnown > -1) { | ||||
|             position = maxLengthKnown; | ||||
|         } | ||||
|         if (position < out.length()) { | ||||
|             out.setLength(position); | ||||
|         } | ||||
|  | ||||
|         dispose(); | ||||
|  | ||||
|         return position; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void write(byte b) throws IOException { | ||||
|         write(new byte[]{b}, 0, 1); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void write(byte b[]) throws IOException { | ||||
|         write(b, 0, b.length); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void write(byte b[], int off, int len) throws IOException { | ||||
|         if (len == 0) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         long end = callback.check(); | ||||
|         int available; | ||||
|  | ||||
|         if (end == -1) { | ||||
|             available = Integer.MAX_VALUE; | ||||
|         } else { | ||||
|             if (end < startOffset) { | ||||
|                 throw new IOException("The reported offset is invalid. reported offset is " + String.valueOf(end)); | ||||
|             } | ||||
|             available = (int) (end - position); | ||||
|         } | ||||
|  | ||||
|         while (available > 0 && auxiliaryBuffers.size() > 0) { | ||||
|             ManagedBuffer aux = auxiliaryBuffers.get(0); | ||||
|  | ||||
|             if ((queue.size + aux.size) > available) { | ||||
|                 available = 0;// wait for next check | ||||
|                 break; | ||||
|             } | ||||
|  | ||||
|             writeQueue(aux.buffer, 0, aux.size); | ||||
|             available -= aux.size; | ||||
|             aux.dereference(); | ||||
|             auxiliaryBuffers.remove(0); | ||||
|         } | ||||
|  | ||||
|         if (available > (len + queue.size)) { | ||||
|             writeQueue(b, off, len); | ||||
|         } else { | ||||
|             int i = auxiliaryBuffers.size() - 1; | ||||
|             while (len > 0) { | ||||
|                 if (i < 0) { | ||||
|                     // allocate a new auxiliary buffer | ||||
|                     auxiliaryBuffers.add(new ManagedBuffer(AUX_BUFFER_SIZE)); | ||||
|                     i++; | ||||
|                 } | ||||
|  | ||||
|                 ManagedBuffer aux = auxiliaryBuffers.get(i); | ||||
|                 available = aux.available(); | ||||
|  | ||||
|                 if (available < 1) { | ||||
|                     // secondary auxiliary buffer | ||||
|                     available = len; | ||||
|                     aux = new ManagedBuffer(Math.max(len, AUX2_BUFFER_SIZE)); | ||||
|                     auxiliaryBuffers.add(aux); | ||||
|                     i++; | ||||
|                 } else { | ||||
|                     available = Math.min(len, available); | ||||
|                 } | ||||
|  | ||||
|                 aux.write(b, off, available); | ||||
|  | ||||
|                 len -= available; | ||||
|                 if (len < 1) { | ||||
|                     break; | ||||
|                 } | ||||
|                 off += available; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void writeOutside(byte buffer[], int offset, int length) throws IOException { | ||||
|         out.write(buffer, offset, length); | ||||
|         position += length; | ||||
|  | ||||
|         if (onProgress != null && position > reportPosition) { | ||||
|             reportPosition = position + AUX2_BUFFER_SIZE;// notify every 256 KiB (approx) | ||||
|             onProgress.report(position); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void writeQueue(byte[] buffer, int offset, int length) throws IOException { | ||||
|         while (length > 0) { | ||||
|             if (queue.available() < length) { | ||||
|                 flushQueue(); | ||||
|  | ||||
|                 if (length >= queue.buffer.length) { | ||||
|                     writeOutside(buffer, offset, length); | ||||
|                     return; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             int size = Math.min(queue.available(), length); | ||||
|             queue.write(buffer, offset, size); | ||||
|  | ||||
|             offset += size; | ||||
|             length -= size; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void flushQueue() throws IOException { | ||||
|         writeOutside(queue.buffer, 0, queue.size); | ||||
|         queue.size = 0; | ||||
|     } | ||||
|  | ||||
|     private void flushEverything() throws IOException { | ||||
|         flushQueue(); | ||||
|  | ||||
|         if (auxiliaryBuffers.size() > 0) { | ||||
|             for (ManagedBuffer aux : auxiliaryBuffers) { | ||||
|                 writeOutside(aux.buffer, 0, aux.size); | ||||
|                 aux.dereference(); | ||||
|             } | ||||
|             auxiliaryBuffers.clear(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Flush any buffer directly to the file. Warning: use this method ONLY if | ||||
|      * all read dependencies are disposed | ||||
|      * | ||||
|      * @throws IOException if the dependencies are not disposed | ||||
|      */ | ||||
|     @Override | ||||
|     public void flush() throws IOException { | ||||
|         if (callback.check() != -1) { | ||||
|             throw new IOException("All read dependencies of this file must be disposed first"); | ||||
|         } | ||||
|         flushEverything(); | ||||
|  | ||||
|         // Save the current file length in case the method {@code rewind()} is called | ||||
|         if (position > maxLengthKnown) { | ||||
|             maxLengthKnown = position; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void rewind() throws IOException { | ||||
|         flush(); | ||||
|         out.seek(startOffset); | ||||
|  | ||||
|         if (onProgress != null) onProgress.report(-position); | ||||
|  | ||||
|         position = startOffset; | ||||
|         reportPosition = startOffset; | ||||
|  | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public long skip(long amount) throws IOException { | ||||
|         flush(); | ||||
|         position += amount; | ||||
|  | ||||
|         out.seek(position); | ||||
|  | ||||
|         return amount; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean isDisposed() { | ||||
|         return out == null; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean canRewind() { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean canWrite() { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     //<editor-fold defaultState="collapsed" desc="stub read methods"> | ||||
|     @Override | ||||
|     public boolean canRead() { | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int read() { | ||||
|         throw new UnsupportedOperationException("write-only"); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int read(byte[] buffer) { | ||||
|         throw new UnsupportedOperationException("write-only"); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int read(byte[] buffer, int offset, int count) { | ||||
|         throw new UnsupportedOperationException("write-only"); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int available() { | ||||
|         throw new UnsupportedOperationException("write-only"); | ||||
|     } | ||||
| //</editor-fold> | ||||
|  | ||||
|     public interface OffsetChecker { | ||||
|  | ||||
|         /** | ||||
|          * Checks the amount of available space ahead | ||||
|          * | ||||
|          * @return absolute offset in the file where no more data SHOULD NOT be | ||||
|          * written. If the value is -1 the whole file will be used | ||||
|          */ | ||||
|         long check(); | ||||
|     } | ||||
|  | ||||
|     public interface ProgressReport { | ||||
|  | ||||
|         void report(long progress); | ||||
|     } | ||||
|  | ||||
|     class ManagedBuffer { | ||||
|  | ||||
|         byte[] buffer; | ||||
|         int size; | ||||
|  | ||||
|         ManagedBuffer(int length) { | ||||
|             buffer = new byte[length]; | ||||
|         } | ||||
|  | ||||
|         void dereference() { | ||||
|             buffer = null; | ||||
|             size = 0; | ||||
|         } | ||||
|  | ||||
|         protected int available() { | ||||
|             return buffer.length - size; | ||||
|         } | ||||
|  | ||||
|         private void write(byte[] b, int off, int len) { | ||||
|             System.arraycopy(b, off, buffer, size, len); | ||||
|             size += len; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public String toString() { | ||||
|             return "holding: " + String.valueOf(size) + " length: " + String.valueOf(buffer.length) + " available: " + String.valueOf(available()); | ||||
|         } | ||||
|  | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,126 @@ | ||||
| package us.shandian.giga.postprocessing.io; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.utils.io.SharpStream; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.io.RandomAccessFile; | ||||
| import java.nio.channels.FileChannel; | ||||
|  | ||||
| /** | ||||
|  * @author kapodamy | ||||
|  */ | ||||
| public class FileStream extends SharpStream { | ||||
|  | ||||
|     public enum Mode { | ||||
|         Read, | ||||
|         ReadWrite | ||||
|     } | ||||
|  | ||||
|     public RandomAccessFile source; | ||||
|     private final Mode mode; | ||||
|  | ||||
|     public FileStream(String path, Mode mode) throws IOException { | ||||
|         String flags; | ||||
|  | ||||
|         if (mode == Mode.Read) { | ||||
|             flags = "r"; | ||||
|         } else { | ||||
|             flags = "rw"; | ||||
|         } | ||||
|  | ||||
|         this.mode = mode; | ||||
|         source = new RandomAccessFile(path, flags); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int read() throws IOException { | ||||
|         return source.read(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     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 { | ||||
|         return source.read(b, off, len); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public long skip(long pos) throws IOException { | ||||
|         FileChannel fc = source.getChannel(); | ||||
|         fc.position(fc.position() + pos); | ||||
|         return pos; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int available() { | ||||
|         try { | ||||
|             return (int) (source.length() - source.getFilePointer()); | ||||
|         } catch (IOException ex) { | ||||
|             return 0; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @SuppressWarnings("EmptyCatchBlock") | ||||
|     @Override | ||||
|     public void dispose() { | ||||
|         try { | ||||
|             source.close(); | ||||
|         } catch (IOException err) { | ||||
|  | ||||
|         } finally { | ||||
|             source = null; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean isDisposed() { | ||||
|         return source == null; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void rewind() throws IOException { | ||||
|         source.getChannel().position(0); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean canRewind() { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean canRead() { | ||||
|         return mode == Mode.Read || mode == Mode.ReadWrite; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean canWrite() { | ||||
|         return mode == Mode.ReadWrite; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void write(byte value) throws IOException { | ||||
|         source.write(value); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void write(byte[] buffer) throws IOException { | ||||
|         source.write(buffer); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void write(byte[] buffer, int offset, int count) throws IOException { | ||||
|         source.write(buffer, offset, count); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void flush() { | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void setLength(long length) throws IOException { | ||||
|         source.setLength(length); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,59 @@ | ||||
| /* | ||||
|  * To change this license header, choose License Headers in Project Properties. | ||||
|  * To change this template file, choose Tools | Templates | ||||
|  * and open the template in the editor. | ||||
|  */ | ||||
| package us.shandian.giga.postprocessing.io; | ||||
|  | ||||
| import android.support.annotation.NonNull; | ||||
|  | ||||
| import org.schabi.newpipe.extractor.utils.io.SharpStream; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
|  | ||||
| /** | ||||
|  * Wrapper for the classic {@link java.io.InputStream} | ||||
|  * @author kapodamy | ||||
|  */ | ||||
| public class SharpInputStream extends InputStream { | ||||
|  | ||||
|     private final SharpStream base; | ||||
|  | ||||
|     public SharpInputStream(SharpStream base) throws IOException { | ||||
|         if (!base.canRead()) { | ||||
|             throw new IOException("The provided stream is not readable"); | ||||
|         } | ||||
|         this.base = base; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int read() throws IOException { | ||||
|         return base.read(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int read(@NonNull byte[] bytes) throws IOException { | ||||
|         return base.read(bytes); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int read(@NonNull byte[] bytes, int i, int i1) throws IOException { | ||||
|         return base.read(bytes, i, i1); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public long skip(long l) throws IOException { | ||||
|         return base.skip(l); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int available() { | ||||
|         return base.available(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void close() { | ||||
|         base.dispose(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										670
									
								
								app/src/main/java/us/shandian/giga/service/DownloadManager.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										670
									
								
								app/src/main/java/us/shandian/giga/service/DownloadManager.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,670 @@ | ||||
| package us.shandian.giga.service; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.content.SharedPreferences; | ||||
| import android.os.Handler; | ||||
| import android.preference.PreferenceManager; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.v7.util.DiffUtil; | ||||
| import android.util.Log; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.Collections; | ||||
| import java.util.Iterator; | ||||
|  | ||||
| import us.shandian.giga.get.DownloadMission; | ||||
| import us.shandian.giga.get.FinishedMission; | ||||
| import us.shandian.giga.get.Mission; | ||||
| import us.shandian.giga.get.sqlite.DownloadDataSource; | ||||
| import us.shandian.giga.util.Utility; | ||||
|  | ||||
| import static org.schabi.newpipe.BuildConfig.DEBUG; | ||||
|  | ||||
| public class DownloadManager { | ||||
|     private static final String TAG = DownloadManager.class.getSimpleName(); | ||||
|  | ||||
|     enum NetworkState {Unavailable, WifiOperating, MobileOperating, OtherOperating} | ||||
|  | ||||
|     public final static int SPECIAL_NOTHING = 0; | ||||
|     public final static int SPECIAL_PENDING = 1; | ||||
|     public final static int SPECIAL_FINISHED = 2; | ||||
|  | ||||
|     private final DownloadDataSource mDownloadDataSource; | ||||
|  | ||||
|     private final ArrayList<DownloadMission> mMissionsPending = new ArrayList<>(); | ||||
|     private final ArrayList<FinishedMission> mMissionsFinished; | ||||
|  | ||||
|     private final Handler mHandler; | ||||
|     private final File mPendingMissionsDir; | ||||
|  | ||||
|     private NetworkState mLastNetworkStatus = NetworkState.Unavailable; | ||||
|  | ||||
|     private SharedPreferences mPrefs; | ||||
|     private String mPrefMaxRetry; | ||||
|     private String mPrefCrossNetwork; | ||||
|  | ||||
|     /** | ||||
|      * Create a new instance | ||||
|      * | ||||
|      * @param context Context for the data source for finished downloads | ||||
|      * @param handler Thread required for Messaging | ||||
|      */ | ||||
|     DownloadManager(@NonNull Context context, Handler handler) { | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "new DownloadManager instance. 0x" + Integer.toHexString(this.hashCode())); | ||||
|         } | ||||
|  | ||||
|         mDownloadDataSource = new DownloadDataSource(context); | ||||
|         mHandler = handler; | ||||
|         mMissionsFinished = loadFinishedMissions(); | ||||
|         mPendingMissionsDir = getPendingDir(context); | ||||
|         mPrefs = PreferenceManager.getDefaultSharedPreferences(context); | ||||
|         mPrefMaxRetry = context.getString(R.string.downloads_max_retry); | ||||
|         mPrefCrossNetwork = context.getString(R.string.cross_network_downloads); | ||||
|  | ||||
|         if (!Utility.mkdir(mPendingMissionsDir, false)) { | ||||
|             throw new RuntimeException("failed to create pending_downloads in data directory"); | ||||
|         } | ||||
|  | ||||
|         loadPendingMissions(); | ||||
|     } | ||||
|  | ||||
|     private static File getPendingDir(@NonNull Context context) { | ||||
|         //File dir = new File(ContextCompat.getDataDir(context), "pending_downloads"); | ||||
|         File dir = context.getExternalFilesDir("pending_downloads"); | ||||
|  | ||||
|         if (dir == null) { | ||||
|             // One of the following paths are not accessible ¿unmounted internal memory? | ||||
|             //        /storage/emulated/0/Android/data/org.schabi.newpipe[.debug]/pending_downloads | ||||
|             //        /sdcard/Android/data/org.schabi.newpipe[.debug]/pending_downloads | ||||
|             Log.w(TAG, "path to pending downloads are not accessible"); | ||||
|         } | ||||
|  | ||||
|         return dir; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Loads finished missions from the data source | ||||
|      */ | ||||
|     private ArrayList<FinishedMission> loadFinishedMissions() { | ||||
|         ArrayList<FinishedMission> finishedMissions = mDownloadDataSource.loadFinishedMissions(); | ||||
|  | ||||
|         // missions always is stored by creation order, simply reverse the list | ||||
|         ArrayList<FinishedMission> result = new ArrayList<>(finishedMissions.size()); | ||||
|         for (int i = finishedMissions.size() - 1; i >= 0; i--) { | ||||
|             FinishedMission mission = finishedMissions.get(i); | ||||
|             File file = mission.getDownloadedFile(); | ||||
|  | ||||
|             if (!file.isFile()) { | ||||
|                 if (DEBUG) { | ||||
|                     Log.d(TAG, "downloaded file removed: " + file.getAbsolutePath()); | ||||
|                 } | ||||
|                 mDownloadDataSource.deleteMission(mission); | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             result.add(mission); | ||||
|         } | ||||
|  | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|     @SuppressWarnings("ResultOfMethodCallIgnored") | ||||
|     private void loadPendingMissions() { | ||||
|         File[] subs = mPendingMissionsDir.listFiles(); | ||||
|  | ||||
|         if (subs == null) { | ||||
|             Log.e(TAG, "listFiles() returned null"); | ||||
|             return; | ||||
|         } | ||||
|         if (subs.length < 1) { | ||||
|             return; | ||||
|         } | ||||
|         if (DEBUG) { | ||||
|             Log.d(TAG, "Loading pending downloads from directory: " + mPendingMissionsDir.getAbsolutePath()); | ||||
|         } | ||||
|  | ||||
|         for (File sub : subs) { | ||||
|             if (sub.isFile()) { | ||||
|                 DownloadMission mis = Utility.readFromFile(sub); | ||||
|  | ||||
|                 if (mis == null) { | ||||
|                     sub.delete(); | ||||
|                 } else { | ||||
|                     if (mis.isFinished()) { | ||||
|                         sub.delete(); | ||||
|                         continue; | ||||
|                     } | ||||
|  | ||||
|                     File dl = mis.getDownloadedFile(); | ||||
|                     boolean exists = dl.exists(); | ||||
|  | ||||
|                     if (mis.postprocessingRunning && mis.postprocessingThis) { | ||||
|                         // Incomplete post-processing results in a corrupted download file | ||||
|                         // because the selected algorithm works on the same file to save space. | ||||
|                         if (!dl.delete()) { | ||||
|                             Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath()); | ||||
|                         } | ||||
|                         exists = true; | ||||
|                         mis.postprocessingRunning = false; | ||||
|                         mis.errCode = DownloadMission.ERROR_POSTPROCESSING_FAILED; | ||||
|                         mis.errObject = new RuntimeException("post-processing stopped unexpectedly"); | ||||
|                     } | ||||
|  | ||||
|                     if (exists && !dl.isFile()) { | ||||
|                         // probably a folder, this should never happens | ||||
|                         if (!sub.delete()) { | ||||
|                             Log.w(TAG, "Unable to delete serialized file: " + sub.getPath()); | ||||
|                         } | ||||
|                         continue; | ||||
|                     } | ||||
|  | ||||
|                     if (!exists) { | ||||
|                         // downloaded file deleted, reset mission state | ||||
|                         DownloadMission m = new DownloadMission(mis.urls, mis.name, mis.location, mis.kind, mis.postprocessingName, mis.postprocessingArgs); | ||||
|                         m.timestamp = mis.timestamp; | ||||
|                         m.threadCount = mis.threadCount; | ||||
|                         m.source = mis.source; | ||||
|                         m.maxRetry = mis.maxRetry; | ||||
|                         mis = m; | ||||
|                     } | ||||
|  | ||||
|                     mis.running = false; | ||||
|                     mis.recovered = exists; | ||||
|                     mis.metadata = sub; | ||||
|                     mis.mHandler = mHandler; | ||||
|  | ||||
|                     mMissionsPending.add(mis); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (mMissionsPending.size() > 1) { | ||||
|             Collections.sort(mMissionsPending, (mission1, mission2) -> Long.compare(mission1.timestamp, mission2.timestamp)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Start a new download mission | ||||
|      * | ||||
|      * @param urls               the list of urls to download | ||||
|      * @param location           the location | ||||
|      * @param name               the name of the file to create | ||||
|      * @param kind               type of file (a: audio  v: video  s: subtitle ?: file-extension defined) | ||||
|      * @param threads            the number of threads maximal used to download chunks of the file. | ||||
|      * @param postprocessingName the name of the required post-processing algorithm, or {@code null} to ignore. | ||||
|      * @param source             source url of the resource | ||||
|      * @param postProcessingArgs the arguments for the post-processing algorithm. | ||||
|      */ | ||||
|     void startMission(String[] urls, String location, String name, char kind, int threads, String source, | ||||
|                       String postprocessingName, String[] postProcessingArgs) { | ||||
|         synchronized (this) { | ||||
|             // check for existing pending download | ||||
|             DownloadMission pendingMission = getPendingMission(location, name); | ||||
|             if (pendingMission != null) { | ||||
|                 // generate unique filename (?) | ||||
|                 try { | ||||
|                     name = generateUniqueName(location, name); | ||||
|                 } catch (Exception e) { | ||||
|                     Log.e(TAG, "Unable to generate unique name", e); | ||||
|                     name = System.currentTimeMillis() + name; | ||||
|                     Log.i(TAG, "Using " + name); | ||||
|                 } | ||||
|             } else { | ||||
|                 // check for existing finished download | ||||
|                 int index = getFinishedMissionIndex(location, name); | ||||
|                 if (index >= 0) mDownloadDataSource.deleteMission(mMissionsFinished.remove(index)); | ||||
|             } | ||||
|  | ||||
|             DownloadMission mission = new DownloadMission(urls, name, location, kind, postprocessingName, postProcessingArgs); | ||||
|             mission.timestamp = System.currentTimeMillis(); | ||||
|             mission.threadCount = threads; | ||||
|             mission.source = source; | ||||
|             mission.mHandler = mHandler; | ||||
|             mission.maxRetry = mPrefs.getInt(mPrefMaxRetry, 3); | ||||
|  | ||||
|             while (true) { | ||||
|                 mission.metadata = new File(mPendingMissionsDir, String.valueOf(mission.timestamp)); | ||||
|                 if (!mission.metadata.isFile() && !mission.metadata.exists()) { | ||||
|                     try { | ||||
|                         if (!mission.metadata.createNewFile()) | ||||
|                             throw new RuntimeException("Cant create download metadata file"); | ||||
|                     } catch (IOException e) { | ||||
|                         throw new RuntimeException(e); | ||||
|                     } | ||||
|                     break; | ||||
|                 } | ||||
|                 mission.timestamp = System.currentTimeMillis(); | ||||
|             } | ||||
|  | ||||
|             mMissionsPending.add(mission); | ||||
|  | ||||
|             // Before starting, save the state in case the internet connection is not available | ||||
|             Utility.writeToFile(mission.metadata, mission); | ||||
|  | ||||
|             if (canDownloadInCurrentNetwork() && (getRunningMissionsCount() < 1)) { | ||||
|                 mission.start(); | ||||
|                 mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_RUNNING); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     public void resumeMission(DownloadMission mission) { | ||||
|         if (!mission.running) { | ||||
|             mission.start(); | ||||
|             mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_RUNNING); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void pauseMission(DownloadMission mission) { | ||||
|         if (mission.running) { | ||||
|             mission.pause(); | ||||
|             mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void deleteMission(Mission mission) { | ||||
|         synchronized (this) { | ||||
|             if (mission instanceof DownloadMission) { | ||||
|                 mMissionsPending.remove(mission); | ||||
|             } else if (mission instanceof FinishedMission) { | ||||
|                 mMissionsFinished.remove(mission); | ||||
|                 mDownloadDataSource.deleteMission(mission); | ||||
|             } | ||||
|  | ||||
|             mission.delete(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Get a pending mission by its location and name | ||||
|      * | ||||
|      * @param location the location | ||||
|      * @param name     the name | ||||
|      * @return the mission or null if no such mission exists | ||||
|      */ | ||||
|     @Nullable | ||||
|     private DownloadMission getPendingMission(String location, String name) { | ||||
|         for (DownloadMission mission : mMissionsPending) { | ||||
|             if (location.equalsIgnoreCase(mission.location) && name.equalsIgnoreCase(mission.name)) { | ||||
|                 return mission; | ||||
|             } | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a finished mission by its location and name | ||||
|      * | ||||
|      * @param location the location | ||||
|      * @param name     the name | ||||
|      * @return the mission index or -1 if no such mission exists | ||||
|      */ | ||||
|     private int getFinishedMissionIndex(String location, String name) { | ||||
|         for (int i = 0; i < mMissionsFinished.size(); i++) { | ||||
|             FinishedMission mission = mMissionsFinished.get(i); | ||||
|             if (location.equalsIgnoreCase(mission.location) && name.equalsIgnoreCase(mission.name)) { | ||||
|                 return i; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return -1; | ||||
|     } | ||||
|  | ||||
|     public Mission getAnyMission(String location, String name) { | ||||
|         synchronized (this) { | ||||
|             Mission mission = getPendingMission(location, name); | ||||
|             if (mission != null) return mission; | ||||
|  | ||||
|             int idx = getFinishedMissionIndex(location, name); | ||||
|             if (idx >= 0) return mMissionsFinished.get(idx); | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     int getRunningMissionsCount() { | ||||
|         int count = 0; | ||||
|         synchronized (this) { | ||||
|             for (DownloadMission mission : mMissionsPending) { | ||||
|                 if (mission.running && mission.errCode != DownloadMission.ERROR_POSTPROCESSING_FAILED && !mission.isFinished()) | ||||
|                     count++; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return count; | ||||
|     } | ||||
|  | ||||
|     void pauseAllMissions() { | ||||
|         synchronized (this) { | ||||
|             for (DownloadMission mission : mMissionsPending) mission.pause(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Splits the filename into name and extension | ||||
|      * <p> | ||||
|      * Dots are ignored if they appear: not at all, at the beginning of the file, | ||||
|      * at the end of the file | ||||
|      * | ||||
|      * @param name the name to split | ||||
|      * @return a string array with a length of 2 containing the name and the extension | ||||
|      */ | ||||
|     private static String[] splitName(String name) { | ||||
|         int dotIndex = name.lastIndexOf('.'); | ||||
|         if (dotIndex <= 0 || (dotIndex == name.length() - 1)) { | ||||
|             return new String[]{name, ""}; | ||||
|         } else { | ||||
|             return new String[]{name.substring(0, dotIndex), name.substring(dotIndex + 1)}; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Generates a unique file name. | ||||
|      * <p> | ||||
|      * e.g. "myName (1).txt" if the name "myName.txt" exists. | ||||
|      * | ||||
|      * @param location the location (to check for existing files) | ||||
|      * @param name     the name of the file | ||||
|      * @return the unique file name | ||||
|      * @throws IllegalArgumentException if the location is not a directory | ||||
|      * @throws SecurityException        if the location is not readable | ||||
|      */ | ||||
|     private static String generateUniqueName(String location, String name) { | ||||
|         if (location == null) throw new NullPointerException("location is null"); | ||||
|         if (name == null) throw new NullPointerException("name is null"); | ||||
|         File destination = new File(location); | ||||
|         if (!destination.isDirectory()) { | ||||
|             throw new IllegalArgumentException("location is not a directory: " + location); | ||||
|         } | ||||
|         final String[] nameParts = splitName(name); | ||||
|         String[] existingName = destination.list((dir, name1) -> name1.startsWith(nameParts[0])); | ||||
|         Arrays.sort(existingName); | ||||
|         String newName; | ||||
|         int downloadIndex = 0; | ||||
|         do { | ||||
|             newName = nameParts[0] + " (" + downloadIndex + ")." + nameParts[1]; | ||||
|             ++downloadIndex; | ||||
|             if (downloadIndex == 1000) {  // Probably an error on our side | ||||
|                 throw new RuntimeException("Too many existing files"); | ||||
|             } | ||||
|         } while (Arrays.binarySearch(existingName, newName) >= 0); | ||||
|         return newName; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Set a pending download as finished | ||||
|      * | ||||
|      * @param mission the desired mission | ||||
|      * @return true if exits pending missions running, otherwise, false | ||||
|      */ | ||||
|     boolean setFinished(DownloadMission mission) { | ||||
|         synchronized (this) { | ||||
|             int i = mMissionsPending.indexOf(mission); | ||||
|             mMissionsPending.remove(i); | ||||
|  | ||||
|             mMissionsFinished.add(0, new FinishedMission(mission)); | ||||
|             mDownloadDataSource.addMission(mission); | ||||
|  | ||||
|             if (mMissionsPending.size() < 1) return false; | ||||
|  | ||||
|             i = getRunningMissionsCount(); | ||||
|             if (i > 0) return true; | ||||
|  | ||||
|             // before returning, check the queue | ||||
|             if (!canDownloadInCurrentNetwork()) return false; | ||||
|  | ||||
|             for (DownloadMission mission1 : mMissionsPending) { | ||||
|                 if (!mission1.running && mission.errCode != DownloadMission.ERROR_POSTPROCESSING_FAILED && mission1.enqueued) { | ||||
|                     resumeMission(mMissionsPending.get(i)); | ||||
|                     return true; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public MissionIterator getIterator() { | ||||
|         return new MissionIterator(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Forget all finished downloads, but, doesn't delete any file | ||||
|      */ | ||||
|     public void forgetFinishedDownloads() { | ||||
|         synchronized (this) { | ||||
|             for (FinishedMission mission : mMissionsFinished) { | ||||
|                 mDownloadDataSource.deleteMission(mission); | ||||
|             } | ||||
|             mMissionsFinished.clear(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private boolean canDownloadInCurrentNetwork() { | ||||
|         if (mLastNetworkStatus == NetworkState.Unavailable) return false; | ||||
|         return !(mPrefs.getBoolean(mPrefCrossNetwork, false) && mLastNetworkStatus == NetworkState.MobileOperating); | ||||
|     } | ||||
|  | ||||
|     void handleConnectivityChange(NetworkState currentStatus) { | ||||
|         if (currentStatus == mLastNetworkStatus) return; | ||||
|  | ||||
|         mLastNetworkStatus = currentStatus; | ||||
|         boolean pauseOnMobile = mPrefs.getBoolean(mPrefCrossNetwork, false); | ||||
|  | ||||
|         if (currentStatus == NetworkState.Unavailable) { | ||||
|             return; | ||||
|         } else if (currentStatus != NetworkState.MobileOperating || !pauseOnMobile) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         boolean flag = false; | ||||
|         synchronized (this) { | ||||
|             for (DownloadMission mission : mMissionsPending) { | ||||
|                 if (mission.running && mission.isFinished() && !mission.postprocessingRunning) { | ||||
|                     flag = true; | ||||
|                     mission.pause(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Fast check for pending downloads. If exists, the user will be notified | ||||
|      * TODO: call this method in somewhere | ||||
|      * | ||||
|      * @param context the application context | ||||
|      */ | ||||
|     public static void notifyUserPendingDownloads(Context context) { | ||||
|         int pending = getPendingDir(context).list().length; | ||||
|         if (pending < 1) return; | ||||
|  | ||||
|         Toast.makeText(context, context.getString( | ||||
|                 R.string.msg_pending_downloads, | ||||
|                 String.valueOf(pending) | ||||
|         ), Toast.LENGTH_LONG).show(); | ||||
|     } | ||||
|  | ||||
|     void checkForRunningMission(String location, String name, DownloadManagerService.DMChecker check) { | ||||
|         boolean listed; | ||||
|         boolean finished = false; | ||||
|  | ||||
|         synchronized (this) { | ||||
|             DownloadMission mission = getPendingMission(location, name); | ||||
|             if (mission != null) { | ||||
|                 listed = true; | ||||
|             } else { | ||||
|                 listed = getFinishedMissionIndex(location, name) >= 0; | ||||
|                 finished = listed; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         check.callback(listed, finished); | ||||
|     } | ||||
|  | ||||
|     public class MissionIterator extends DiffUtil.Callback { | ||||
|         final Object FINISHED = new Object(); | ||||
|         final Object PENDING = new Object(); | ||||
|  | ||||
|         ArrayList<Object> snapshot; | ||||
|         ArrayList<Object> current; | ||||
|         ArrayList<Mission> hidden; | ||||
|  | ||||
|         private MissionIterator() { | ||||
|             hidden = new ArrayList<>(2); | ||||
|             current = null; | ||||
|             snapshot = getSpecialItems(); | ||||
|         } | ||||
|  | ||||
|         private ArrayList<Object> getSpecialItems() { | ||||
|             synchronized (DownloadManager.this) { | ||||
|                 ArrayList<Mission> pending = new ArrayList<>(mMissionsPending); | ||||
|                 ArrayList<Mission> finished = new ArrayList<>(mMissionsFinished); | ||||
|                 ArrayList<Mission> remove = new ArrayList<>(hidden); | ||||
|  | ||||
|                 // hide missions (if required) | ||||
|                 Iterator<Mission> iterator = remove.iterator(); | ||||
|                 while (iterator.hasNext()) { | ||||
|                     Mission mission = iterator.next(); | ||||
|                     if (pending.remove(mission) || finished.remove(mission)) iterator.remove(); | ||||
|                 } | ||||
|  | ||||
|                 int fakeTotal = pending.size(); | ||||
|                 if (fakeTotal > 0) fakeTotal++; | ||||
|  | ||||
|                 fakeTotal += finished.size(); | ||||
|                 if (finished.size() > 0) fakeTotal++; | ||||
|  | ||||
|                 ArrayList<Object> list = new ArrayList<>(fakeTotal); | ||||
|                 if (pending.size() > 0) { | ||||
|                     list.add(PENDING); | ||||
|                     list.addAll(pending); | ||||
|                 } | ||||
|                 if (finished.size() > 0) { | ||||
|                     list.add(FINISHED); | ||||
|                     list.addAll(finished); | ||||
|                 } | ||||
|  | ||||
|  | ||||
|                 return list; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public MissionItem getItem(int position) { | ||||
|             Object object = snapshot.get(position); | ||||
|  | ||||
|             if (object == PENDING) return new MissionItem(SPECIAL_PENDING); | ||||
|             if (object == FINISHED) return new MissionItem(SPECIAL_FINISHED); | ||||
|  | ||||
|             return new MissionItem(SPECIAL_NOTHING, (Mission) object); | ||||
|         } | ||||
|  | ||||
|         public int getSpecialAtItem(int position) { | ||||
|             Object object = snapshot.get(position); | ||||
|  | ||||
|             if (object == PENDING) return SPECIAL_PENDING; | ||||
|             if (object == FINISHED) return SPECIAL_FINISHED; | ||||
|  | ||||
|             return SPECIAL_NOTHING; | ||||
|         } | ||||
|  | ||||
|         public MissionItem getItemUnsafe(int position) { | ||||
|             synchronized (DownloadManager.this) { | ||||
|                 int count = mMissionsPending.size(); | ||||
|                 int count2 = mMissionsFinished.size(); | ||||
|  | ||||
|                 if (count > 0) { | ||||
|                     position--; | ||||
|                     if (position == -1) | ||||
|                         return new MissionItem(SPECIAL_PENDING); | ||||
|                     else if (position < count) | ||||
|                         return new MissionItem(SPECIAL_NOTHING, mMissionsPending.get(position)); | ||||
|                     else if (position == count && count2 > 0) | ||||
|                         return new MissionItem(SPECIAL_FINISHED); | ||||
|                     else | ||||
|                         position -= count; | ||||
|                 } else { | ||||
|                     if (count2 > 0 && position == 0) { | ||||
|                         return new MissionItem(SPECIAL_FINISHED); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 position--; | ||||
|  | ||||
|                 if (count2 < 1) { | ||||
|                     throw new RuntimeException( | ||||
|                             String.format("Out of range. pending_count=%s  finished_count=%s  position=%s", count, count2, position) | ||||
|                     ); | ||||
|                 } | ||||
|  | ||||
|                 return new MissionItem(SPECIAL_NOTHING, mMissionsFinished.get(position)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|  | ||||
|         public void start() { | ||||
|             current = getSpecialItems(); | ||||
|         } | ||||
|  | ||||
|         public void end() { | ||||
|             snapshot = current; | ||||
|             current = null; | ||||
|         } | ||||
|  | ||||
|         public void hide(Mission mission) { | ||||
|             hidden.add(mission); | ||||
|         } | ||||
|  | ||||
|         public void unHide(Mission mission) { | ||||
|             hidden.remove(mission); | ||||
|         } | ||||
|  | ||||
|  | ||||
|         @Override | ||||
|         public int getOldListSize() { | ||||
|             return snapshot.size(); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public int getNewListSize() { | ||||
|             return current.size(); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { | ||||
|             return snapshot.get(oldItemPosition) == current.get(newItemPosition); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { | ||||
|             return areItemsTheSame(oldItemPosition, newItemPosition); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public class MissionItem { | ||||
|         public int special; | ||||
|         public Mission mission; | ||||
|  | ||||
|         MissionItem(int s, Mission m) { | ||||
|             special = s; | ||||
|             mission = m; | ||||
|         } | ||||
|  | ||||
|         MissionItem(int s) { | ||||
|             this(s, null); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -2,17 +2,25 @@ package us.shandian.giga.service; | ||||
|  | ||||
| import android.Manifest; | ||||
| import android.app.Notification; | ||||
| import android.app.NotificationManager; | ||||
| import android.app.PendingIntent; | ||||
| import android.app.Service; | ||||
| import android.content.BroadcastReceiver; | ||||
| import android.content.ComponentName; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.content.IntentFilter; | ||||
| import android.content.ServiceConnection; | ||||
| import android.graphics.Bitmap; | ||||
| import android.graphics.BitmapFactory; | ||||
| import android.net.ConnectivityManager; | ||||
| import android.net.NetworkInfo; | ||||
| import android.net.Uri; | ||||
| import android.os.Binder; | ||||
| import android.os.Build; | ||||
| import android.os.Handler; | ||||
| import android.os.HandlerThread; | ||||
| import android.os.IBinder; | ||||
| import android.os.Looper; | ||||
| import android.os.Message; | ||||
| import android.support.v4.app.NotificationCompat.Builder; | ||||
| import android.support.v4.content.PermissionChecker; | ||||
| @@ -21,48 +29,61 @@ import android.widget.Toast; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.download.DownloadActivity; | ||||
| import org.schabi.newpipe.settings.NewPipeSettings; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Iterator; | ||||
|  | ||||
| import us.shandian.giga.get.DownloadDataSource; | ||||
| import us.shandian.giga.get.DownloadManager; | ||||
| import us.shandian.giga.get.DownloadManagerImpl; | ||||
| import us.shandian.giga.get.DownloadMission; | ||||
| import us.shandian.giga.get.sqlite.SQLiteDownloadDataSource; | ||||
| import us.shandian.giga.service.DownloadManager.NetworkState; | ||||
|  | ||||
| import static org.schabi.newpipe.BuildConfig.APPLICATION_ID; | ||||
| import static org.schabi.newpipe.BuildConfig.DEBUG; | ||||
|  | ||||
| public class DownloadManagerService extends Service { | ||||
|  | ||||
|     private static final String TAG = DownloadManagerService.class.getSimpleName(); | ||||
|  | ||||
|     /** | ||||
|      * Message code of update messages stored as {@link Message#what}. | ||||
|      */ | ||||
|     private static final int UPDATE_MESSAGE = 0; | ||||
|     private static final int NOTIFICATION_ID = 1000; | ||||
|     public static final int MESSAGE_RUNNING = 1; | ||||
|     public static final int MESSAGE_PAUSED = 2; | ||||
|     public static final int MESSAGE_FINISHED = 3; | ||||
|     public static final int MESSAGE_PROGRESS = 4; | ||||
|     public static final int MESSAGE_ERROR = 5; | ||||
|  | ||||
|     private static final int FOREGROUND_NOTIFICATION_ID = 1000; | ||||
|     private static final int DOWNLOADS_NOTIFICATION_ID = 1001; | ||||
|  | ||||
|     private static final String EXTRA_URLS = "DownloadManagerService.extra.urls"; | ||||
|     private static final String EXTRA_NAME = "DownloadManagerService.extra.name"; | ||||
|     private static final String EXTRA_LOCATION = "DownloadManagerService.extra.location"; | ||||
|     private static final String EXTRA_IS_AUDIO = "DownloadManagerService.extra.is_audio"; | ||||
|     private static final String EXTRA_KIND = "DownloadManagerService.extra.kind"; | ||||
|     private static final String EXTRA_THREADS = "DownloadManagerService.extra.threads"; | ||||
|     private static final String EXTRA_POSTPROCESSING_NAME = "DownloadManagerService.extra.postprocessingName"; | ||||
|     private static final String EXTRA_POSTPROCESSING_ARGS = "DownloadManagerService.extra.postprocessingArgs"; | ||||
|     private static final String EXTRA_SOURCE = "DownloadManagerService.extra.source"; | ||||
|  | ||||
|     private static final String ACTION_RESET_DOWNLOAD_COUNT = APPLICATION_ID + ".reset_download_count"; | ||||
|  | ||||
|     private DMBinder mBinder; | ||||
|     private DownloadManager mManager; | ||||
|     private Notification mNotification; | ||||
|     private Handler mHandler; | ||||
|     private long mLastTimeStamp = System.currentTimeMillis(); | ||||
|     private DownloadDataSource mDataSource; | ||||
|     private int downloadDoneCount = 0; | ||||
|     private Builder downloadDoneNotification = null; | ||||
|     private StringBuilder downloadDoneList = null; | ||||
|     NotificationManager notificationManager = null; | ||||
|     private boolean mForeground = false; | ||||
|     private final ArrayList<Handler> mEchoObservers = new ArrayList<>(1); | ||||
|  | ||||
|     private BroadcastReceiver mNetworkStateListener; | ||||
|  | ||||
|     private final MissionListener missionListener = new MissionListener(); | ||||
|  | ||||
|  | ||||
|     private void notifyMediaScanner(DownloadMission mission) { | ||||
|         Uri uri = Uri.parse("file://" + mission.location + "/" + mission.name); | ||||
|         // notify media scanner on downloaded media file ... | ||||
|         sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri)); | ||||
|     /** | ||||
|      * notify media scanner on downloaded media file ... | ||||
|      * | ||||
|      * @param file the downloaded file | ||||
|      */ | ||||
|     private void notifyMediaScanner(File file) { | ||||
|         sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file))); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -74,19 +95,14 @@ public class DownloadManagerService extends Service { | ||||
|         } | ||||
|  | ||||
|         mBinder = new DMBinder(); | ||||
|         if (mDataSource == null) { | ||||
|             mDataSource = new SQLiteDownloadDataSource(this); | ||||
|         } | ||||
|         if (mManager == null) { | ||||
|             ArrayList<String> paths = new ArrayList<>(2); | ||||
|             paths.add(NewPipeSettings.getVideoDownloadPath(this)); | ||||
|             paths.add(NewPipeSettings.getAudioDownloadPath(this)); | ||||
|             mManager = new DownloadManagerImpl(paths, mDataSource, this); | ||||
|             if (DEBUG) { | ||||
|                 Log.d(TAG, "mManager == null"); | ||||
|                 Log.d(TAG, "Download directory: " + paths); | ||||
|             } | ||||
|         mHandler = new Handler(Looper.myLooper()) { | ||||
|             @Override | ||||
|             public void handleMessage(Message msg) { | ||||
|                 DownloadManagerService.this.handleMessage(msg); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         mManager = new DownloadManager(this, mHandler); | ||||
|  | ||||
|         Intent openDownloadListIntent = new Intent(this, DownloadActivity.class) | ||||
|                 .setAction(Intent.ACTION_MAIN); | ||||
| @@ -105,56 +121,49 @@ public class DownloadManagerService extends Service { | ||||
|                 .setContentText(getString(R.string.msg_running_detail)); | ||||
|  | ||||
|         mNotification = builder.build(); | ||||
|         notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); | ||||
|  | ||||
|         HandlerThread thread = new HandlerThread("ServiceMessenger"); | ||||
|         thread.start(); | ||||
|  | ||||
|         mHandler = new Handler(thread.getLooper()) { | ||||
|         mNetworkStateListener = new BroadcastReceiver() { | ||||
|             @Override | ||||
|             public void handleMessage(Message msg) { | ||||
|                 switch (msg.what) { | ||||
|                     case UPDATE_MESSAGE: { | ||||
|                         int runningCount = 0; | ||||
|  | ||||
|                         for (int i = 0; i < mManager.getCount(); i++) { | ||||
|                             if (mManager.getMission(i).running) { | ||||
|                                 runningCount++; | ||||
|                             } | ||||
|                         } | ||||
|                         updateState(runningCount); | ||||
|                         break; | ||||
|                     } | ||||
|             public void onReceive(Context context, Intent intent) { | ||||
|                 if (intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false)) { | ||||
|                     handleConnectivityChange(null); | ||||
|                     return; | ||||
|                 } | ||||
|                 handleConnectivityChange(intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO)); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|     } | ||||
|  | ||||
|     private void startMissionAsync(final String url, final String location, final String name, | ||||
|                                    final boolean isAudio, final int threads) { | ||||
|         mHandler.post(new Runnable() { | ||||
|             @Override | ||||
|             public void run() { | ||||
|                 int missionId = mManager.startMission(url, location, name, isAudio, threads); | ||||
|                 mBinder.onMissionAdded(mManager.getMission(missionId)); | ||||
|             } | ||||
|         }); | ||||
|         registerReceiver(mNetworkStateListener, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int onStartCommand(Intent intent, int flags, int startId) { | ||||
|         if (DEBUG) { | ||||
|             if (intent == null) { | ||||
|                 Log.d(TAG, "Restarting"); | ||||
|                 return START_NOT_STICKY; | ||||
|             } | ||||
|             Log.d(TAG, "Starting"); | ||||
|         } | ||||
|         Log.i(TAG, "Got intent: " + intent); | ||||
|         String action = intent.getAction(); | ||||
|         if (action != null && action.equals(Intent.ACTION_RUN)) { | ||||
|         if (action != null) { | ||||
|             if (action.equals(Intent.ACTION_RUN)) { | ||||
|                 String[] urls = intent.getStringArrayExtra(EXTRA_URLS); | ||||
|                 String name = intent.getStringExtra(EXTRA_NAME); | ||||
|                 String location = intent.getStringExtra(EXTRA_LOCATION); | ||||
|                 int threads = intent.getIntExtra(EXTRA_THREADS, 1); | ||||
|             boolean isAudio = intent.getBooleanExtra(EXTRA_IS_AUDIO, false); | ||||
|             String url = intent.getDataString(); | ||||
|             startMissionAsync(url, location, name, isAudio, threads); | ||||
|                 char kind = intent.getCharExtra(EXTRA_KIND, '?'); | ||||
|                 String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME); | ||||
|                 String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS); | ||||
|                 String source = intent.getStringExtra(EXTRA_SOURCE); | ||||
|  | ||||
|                 mHandler.post(() -> mManager.startMission(urls, location, name, kind, threads, source, psName, psArgs)); | ||||
|  | ||||
|             } else if (downloadDoneNotification != null && action.equals(ACTION_RESET_DOWNLOAD_COUNT)) { | ||||
|                 downloadDoneCount = 0; | ||||
|                 downloadDoneList.setLength(0); | ||||
|             } | ||||
|         } | ||||
|         return START_NOT_STICKY; | ||||
|     } | ||||
| @@ -167,11 +176,17 @@ public class DownloadManagerService extends Service { | ||||
|             Log.d(TAG, "Destroying"); | ||||
|         } | ||||
|  | ||||
|         for (int i = 0; i < mManager.getCount(); i++) { | ||||
|             mManager.pauseMission(i); | ||||
|         stopForeground(true); | ||||
|  | ||||
|         if (notificationManager != null && downloadDoneNotification != null) { | ||||
|             downloadDoneNotification.setDeleteIntent(null);// prevent NewPipe running when is killed, cleared from recent, etc | ||||
|             notificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build()); | ||||
|         } | ||||
|  | ||||
|         stopForeground(true); | ||||
|         unregisterReceiver(mNetworkStateListener); | ||||
|  | ||||
|         mManager.pauseAllMissions(); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -192,53 +207,171 @@ public class DownloadManagerService extends Service { | ||||
|         return mBinder; | ||||
|     } | ||||
|  | ||||
|     private void postUpdateMessage() { | ||||
|         mHandler.sendEmptyMessage(UPDATE_MESSAGE); | ||||
|     public void handleMessage(Message msg) { | ||||
|         switch (msg.what) { | ||||
|             case MESSAGE_FINISHED: | ||||
|                 DownloadMission mission = (DownloadMission) msg.obj; | ||||
|                 notifyMediaScanner(mission.getDownloadedFile()); | ||||
|                 notifyFinishedDownload(mission.name); | ||||
|                 updateForegroundState(mManager.setFinished(mission)); | ||||
|                 break; | ||||
|             case MESSAGE_RUNNING: | ||||
|             case MESSAGE_PROGRESS: | ||||
|                 updateForegroundState(true); | ||||
|                 break; | ||||
|             case MESSAGE_PAUSED: | ||||
|             case MESSAGE_ERROR: | ||||
|                 updateForegroundState(mManager.getRunningMissionsCount() > 0); | ||||
|                 break; | ||||
|         } | ||||
|  | ||||
|     private void updateState(int runningCount) { | ||||
|         if (runningCount == 0) { | ||||
|             stopForeground(true); | ||||
|  | ||||
|         synchronized (mEchoObservers) { | ||||
|             Iterator<Handler> iterator = mEchoObservers.iterator(); | ||||
|             while (iterator.hasNext()) { | ||||
|                 Handler handler = iterator.next(); | ||||
|                 if (handler.getLooper().getThread().isAlive()) { | ||||
|                     Message echo = new Message(); | ||||
|                     echo.what = msg.what; | ||||
|                     echo.obj = msg.obj; | ||||
|                     handler.sendMessage(echo); | ||||
|                 } else { | ||||
|             startForeground(NOTIFICATION_ID, mNotification); | ||||
|                     iterator.remove();// ¿missing call to removeMissionEventListener()? | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static void startMission(Context context, String url, String location, String name, boolean isAudio, int threads) { | ||||
|     private void handleConnectivityChange(NetworkInfo info) { | ||||
|         NetworkState status; | ||||
|  | ||||
|         if (info == null) { | ||||
|             status = NetworkState.Unavailable; | ||||
|             Log.i(TAG, "actual connectivity status is unavailable"); | ||||
|         } else if (!info.isAvailable() || !info.isConnected()) { | ||||
|             status = NetworkState.Unavailable; | ||||
|             Log.i(TAG, "actual connectivity status is not available and not connected"); | ||||
|         } else { | ||||
|             int type = info.getType(); | ||||
|             if (type == ConnectivityManager.TYPE_MOBILE || type == ConnectivityManager.TYPE_MOBILE_DUN) { | ||||
|                 status = NetworkState.MobileOperating; | ||||
|             } else if (type == ConnectivityManager.TYPE_WIFI) { | ||||
|                 status = NetworkState.WifiOperating; | ||||
|             } else if (type == ConnectivityManager.TYPE_WIMAX || | ||||
|                     type == ConnectivityManager.TYPE_ETHERNET || | ||||
|                     type == ConnectivityManager.TYPE_BLUETOOTH) { | ||||
|                 status = NetworkState.OtherOperating; | ||||
|             } else { | ||||
|                 status = NetworkState.Unavailable; | ||||
|             } | ||||
|             Log.i(TAG, "actual connectivity status is " + status.name()); | ||||
|         } | ||||
|  | ||||
|         if (mManager == null) return;// avoid race-conditions while the service is starting | ||||
|         mManager.handleConnectivityChange(status); | ||||
|     } | ||||
|  | ||||
|     public void updateForegroundState(boolean state) { | ||||
|         if (state == mForeground) return; | ||||
|  | ||||
|         if (state) { | ||||
|             startForeground(FOREGROUND_NOTIFICATION_ID, mNotification); | ||||
|         } else { | ||||
|             stopForeground(true); | ||||
|         } | ||||
|  | ||||
|         mForeground = state; | ||||
|     } | ||||
|  | ||||
|     public static void startMission(Context context, String urls[], String location, String name, | ||||
|                                     char kind, int threads, String source, String postprocessingName, | ||||
|                                     String[] postprocessingArgs) { | ||||
|         Intent intent = new Intent(context, DownloadManagerService.class); | ||||
|         intent.setAction(Intent.ACTION_RUN); | ||||
|         intent.setData(Uri.parse(url)); | ||||
|         intent.putExtra(EXTRA_URLS, urls); | ||||
|         intent.putExtra(EXTRA_NAME, name); | ||||
|         intent.putExtra(EXTRA_LOCATION, location); | ||||
|         intent.putExtra(EXTRA_IS_AUDIO, isAudio); | ||||
|         intent.putExtra(EXTRA_KIND, kind); | ||||
|         intent.putExtra(EXTRA_THREADS, threads); | ||||
|         intent.putExtra(EXTRA_SOURCE, source); | ||||
|         intent.putExtra(EXTRA_POSTPROCESSING_NAME, postprocessingName); | ||||
|         intent.putExtra(EXTRA_POSTPROCESSING_ARGS, postprocessingArgs); | ||||
|         context.startService(intent); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private class MissionListener implements DownloadMission.MissionListener { | ||||
|     public static void checkForRunningMission(Context context, String location, String name, DMChecker check) { | ||||
|         Intent intent = new Intent(); | ||||
|         intent.setClass(context, DownloadManagerService.class); | ||||
|         context.bindService(intent, new ServiceConnection() { | ||||
|             @Override | ||||
|         public void onProgressUpdate(DownloadMission downloadMission, long done, long total) { | ||||
|             long now = System.currentTimeMillis(); | ||||
|             long delta = now - mLastTimeStamp; | ||||
|             if (delta > 2000) { | ||||
|                 postUpdateMessage(); | ||||
|                 mLastTimeStamp = now; | ||||
|             public void onServiceConnected(ComponentName cname, IBinder service) { | ||||
|                 try { | ||||
|                     ((DMBinder) service).getDownloadManager().checkForRunningMission(location, name, check); | ||||
|                 } catch (Exception err) { | ||||
|                     Log.w(TAG, "checkForRunningMission() callback is defective", err); | ||||
|                 } | ||||
|  | ||||
|                 // TODO: find a efficient way to unbind the service. This destroy the service due idle, but is started again when the user start a download. | ||||
|                 context.unbindService(this); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|         public void onFinish(DownloadMission downloadMission) { | ||||
|             postUpdateMessage(); | ||||
|             notifyMediaScanner(downloadMission); | ||||
|             public void onServiceDisconnected(ComponentName name) { | ||||
|             } | ||||
|         }, Context.BIND_AUTO_CREATE); | ||||
|     } | ||||
|  | ||||
|         @Override | ||||
|         public void onError(DownloadMission downloadMission, int errCode) { | ||||
|             postUpdateMessage(); | ||||
|         } | ||||
|     public void notifyFinishedDownload(String name) { | ||||
|         if (notificationManager == null) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (downloadDoneNotification == null) { | ||||
|             downloadDoneList = new StringBuilder(name.length()); | ||||
|  | ||||
|             Bitmap icon = BitmapFactory.decodeResource(this.getResources(), android.R.drawable.stat_sys_download_done); | ||||
|             downloadDoneNotification = new Builder(this, getString(R.string.notification_channel_id)) | ||||
|                     .setAutoCancel(true) | ||||
|                     .setLargeIcon(icon) | ||||
|                     .setSmallIcon(android.R.drawable.stat_sys_download_done) | ||||
|                     .setDeleteIntent(PendingIntent.getService(this, (int) System.currentTimeMillis(), | ||||
|                             new Intent(this, DownloadManagerService.class) | ||||
|                                     .setAction(ACTION_RESET_DOWNLOAD_COUNT) | ||||
|                             , PendingIntent.FLAG_UPDATE_CURRENT)) | ||||
|                     .setContentIntent(mNotification.contentIntent); | ||||
|         } | ||||
|  | ||||
|         if (downloadDoneCount < 1) { | ||||
|             downloadDoneList.append(name); | ||||
|  | ||||
|             if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { | ||||
|                 downloadDoneNotification.setContentTitle(getString(R.string.app_name)); | ||||
|                 downloadDoneNotification.setContentText(getString(R.string.download_finished, name)); | ||||
|             } else { | ||||
|                 downloadDoneNotification.setContentTitle(getString(R.string.download_finished, name)); | ||||
|                 downloadDoneNotification.setContentText(null); | ||||
|             } | ||||
|         } else { | ||||
|             downloadDoneList.append(", "); | ||||
|             downloadDoneList.append(name); | ||||
|  | ||||
|             downloadDoneNotification.setContentTitle(getString(R.string.download_finished_more, String.valueOf(downloadDoneCount + 1))); | ||||
|             downloadDoneNotification.setContentText(downloadDoneList.toString()); | ||||
|         } | ||||
|  | ||||
|         notificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build()); | ||||
|         downloadDoneCount++; | ||||
|     } | ||||
|  | ||||
|     private void manageObservers(Handler handler, boolean add) { | ||||
|         synchronized (mEchoObservers) { | ||||
|             if (add) { | ||||
|                mEchoObservers.add(handler); | ||||
|             } else { | ||||
|                 mEchoObservers.remove(handler); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Wrapper of DownloadManager | ||||
|     public class DMBinder extends Binder { | ||||
| @@ -246,14 +379,24 @@ public class DownloadManagerService extends Service { | ||||
|             return mManager; | ||||
|         } | ||||
|  | ||||
|         public void onMissionAdded(DownloadMission mission) { | ||||
|             mission.addListener(missionListener); | ||||
|             postUpdateMessage(); | ||||
|         public void addMissionEventListener(Handler handler) { | ||||
|             manageObservers(handler, true); | ||||
|         } | ||||
|  | ||||
|         public void onMissionRemoved(DownloadMission mission) { | ||||
|             mission.removeListener(missionListener); | ||||
|             postUpdateMessage(); | ||||
|         public void removeMissionEventListener(Handler handler) { | ||||
|             manageObservers(handler, false); | ||||
|         } | ||||
|  | ||||
|         public void resetFinishedDownloadCount() { | ||||
|             if (notificationManager == null || downloadDoneNotification == null) return; | ||||
|             notificationManager.cancel(DOWNLOADS_NOTIFICATION_ID); | ||||
|             downloadDoneList.setLength(0); | ||||
|             downloadDoneCount = 0; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public interface DMChecker { | ||||
|         void callback(boolean listed, boolean finished); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| package us.shandian.giga.ui.adapter; | ||||
|  | ||||
| import android.annotation.SuppressLint; | ||||
| import android.app.Activity; | ||||
| import android.app.ProgressDialog; | ||||
| import android.content.Context; | ||||
| @@ -7,12 +8,20 @@ import android.content.Intent; | ||||
| import android.net.Uri; | ||||
| import android.os.AsyncTask; | ||||
| import android.os.Build; | ||||
| import android.os.Bundle; | ||||
| import android.os.Handler; | ||||
| import android.os.Looper; | ||||
| import android.os.Message; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.v4.content.FileProvider; | ||||
| import android.support.v4.view.ViewCompat; | ||||
| import android.support.v7.app.AlertDialog; | ||||
| import android.support.v7.util.DiffUtil; | ||||
| import android.support.v7.widget.RecyclerView; | ||||
| import android.support.v7.widget.RecyclerView.ViewHolder; | ||||
| import android.util.Log; | ||||
| import android.util.SparseArray; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.Menu; | ||||
| import android.view.MenuItem; | ||||
| @@ -24,28 +33,28 @@ import android.widget.PopupMenu; | ||||
| import android.widget.TextView; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import org.schabi.newpipe.BuildConfig; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.download.DeleteDownloadManager; | ||||
| import org.schabi.newpipe.util.NavigationHelper; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.lang.ref.WeakReference; | ||||
| import java.util.ArrayList; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
| import java.util.Map; | ||||
|  | ||||
| import us.shandian.giga.get.DownloadManager; | ||||
| import us.shandian.giga.get.DownloadMission; | ||||
| import us.shandian.giga.get.FinishedMission; | ||||
| import us.shandian.giga.service.DownloadManager; | ||||
| import us.shandian.giga.service.DownloadManagerService; | ||||
| import us.shandian.giga.ui.common.Deleter; | ||||
| import us.shandian.giga.ui.common.ProgressDrawable; | ||||
| import us.shandian.giga.util.Utility; | ||||
|  | ||||
| import static android.content.Intent.FLAG_GRANT_PREFIX_URI_PERMISSION; | ||||
| import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; | ||||
|  | ||||
| public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHolder> { | ||||
|     private static final Map<Integer, String> ALGORITHMS = new HashMap<>(); | ||||
| public class MissionAdapter extends RecyclerView.Adapter<ViewHolder> { | ||||
|     private static final SparseArray<String> ALGORITHMS = new SparseArray<>(); | ||||
|     private static final String TAG = "MissionAdapter"; | ||||
|  | ||||
|     static { | ||||
| @@ -53,109 +62,131 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold | ||||
|         ALGORITHMS.put(R.id.sha1, "SHA1"); | ||||
|     } | ||||
|  | ||||
|     private Activity mContext; | ||||
|     private Context mContext; | ||||
|     private LayoutInflater mInflater; | ||||
|     private DownloadManager mDownloadManager; | ||||
|     private DeleteDownloadManager mDeleteDownloadManager; | ||||
|     private List<DownloadMission> mItemList; | ||||
|     private DownloadManagerService.DMBinder mBinder; | ||||
|     private Deleter mDeleter; | ||||
|     private int mLayout; | ||||
|     private DownloadManager.MissionIterator mIterator; | ||||
|     private Handler mHandler; | ||||
|     private ArrayList<ViewHolderItem> mPendingDownloadsItems = new ArrayList<>(); | ||||
|     private MenuItem mClear; | ||||
|     private View mEmptyMessage; | ||||
|  | ||||
|     public MissionAdapter(Activity context, DownloadManagerService.DMBinder binder, DownloadManager downloadManager, DeleteDownloadManager deleteDownloadManager, boolean isLinear) { | ||||
|     public MissionAdapter(Context context, DownloadManager downloadManager, MenuItem clearButton, View emptyMessage) { | ||||
|         mContext = context; | ||||
|         mDownloadManager = downloadManager; | ||||
|         mDeleteDownloadManager = deleteDownloadManager; | ||||
|         mBinder = binder; | ||||
|         mDeleter = null; | ||||
|  | ||||
|         mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); | ||||
|         mLayout = isLinear ? R.layout.mission_item_linear : R.layout.mission_item; | ||||
|         mLayout = R.layout.mission_item; | ||||
|  | ||||
|         mItemList = new ArrayList<>(); | ||||
|         updateItemList(); | ||||
|     } | ||||
|  | ||||
|     public void updateItemList() { | ||||
|         mItemList.clear(); | ||||
|  | ||||
|         for (int i = 0; i < mDownloadManager.getCount(); i++) { | ||||
|             DownloadMission mission = mDownloadManager.getMission(i); | ||||
|             if (!mDeleteDownloadManager.contains(mission)) { | ||||
|                 mItemList.add(mDownloadManager.getMission(i)); | ||||
|         mHandler = new Handler(Looper.myLooper()) { | ||||
|             @Override | ||||
|             public void handleMessage(Message msg) { | ||||
|                 switch (msg.what) { | ||||
|                     case DownloadManagerService.MESSAGE_PROGRESS: | ||||
|                     case DownloadManagerService.MESSAGE_ERROR: | ||||
|                     case DownloadManagerService.MESSAGE_FINISHED: | ||||
|                         onServiceMessage(msg); | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         mClear = clearButton; | ||||
|         mEmptyMessage = emptyMessage; | ||||
|  | ||||
|         mIterator = downloadManager.getIterator(); | ||||
|  | ||||
|         checkEmptyMessageVisibility(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public MissionAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { | ||||
|         final ViewHolder h = new ViewHolder(mInflater.inflate(mLayout, parent, false)); | ||||
|  | ||||
|         h.menu.setOnClickListener(new View.OnClickListener() { | ||||
|             @Override | ||||
|             public void onClick(View v) { | ||||
|                 buildPopup(h); | ||||
|     @NonNull | ||||
|     public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { | ||||
|         switch (viewType) { | ||||
|             case DownloadManager.SPECIAL_PENDING: | ||||
|             case DownloadManager.SPECIAL_FINISHED: | ||||
|                 return new ViewHolderHeader(mInflater.inflate(R.layout.missions_header, parent, false)); | ||||
|         } | ||||
|         }); | ||||
|  | ||||
| 		/*h.itemView.setOnClickListener(new View.OnClickListener() { | ||||
|             @Override | ||||
| 			public void onClick(View v) { | ||||
| 				showDetail(h); | ||||
| 			} | ||||
| 		});*/ | ||||
|  | ||||
|         return h; | ||||
|         return new ViewHolderItem(mInflater.inflate(mLayout, parent, false)); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onViewRecycled(MissionAdapter.ViewHolder h) { | ||||
|         super.onViewRecycled(h); | ||||
|         h.mission.removeListener(h.observer); | ||||
|         h.mission = null; | ||||
|         h.observer = null; | ||||
|         h.progress = null; | ||||
|         h.position = -1; | ||||
|     public void onViewRecycled(@NonNull ViewHolder view) { | ||||
|         super.onViewRecycled(view); | ||||
|  | ||||
|         if (view instanceof ViewHolderHeader) return; | ||||
|         ViewHolderItem h = (ViewHolderItem) view; | ||||
|  | ||||
|         if (h.item.mission instanceof DownloadMission) mPendingDownloadsItems.remove(h); | ||||
|  | ||||
|         h.popupMenu.dismiss(); | ||||
|         h.item = null; | ||||
|         h.lastTimeStamp = -1; | ||||
|         h.lastDone = -1; | ||||
|         h.colorId = 0; | ||||
|         h.lastCurrent = -1; | ||||
|         h.state = 0; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onBindViewHolder(MissionAdapter.ViewHolder h, int pos) { | ||||
|         DownloadMission ms = mItemList.get(pos); | ||||
|         h.mission = ms; | ||||
|         h.position = pos; | ||||
|     @SuppressLint("SetTextI18n") | ||||
|     public void onBindViewHolder(@NonNull ViewHolder view, @SuppressLint("RecyclerView") int pos) { | ||||
|         DownloadManager.MissionItem item = mIterator.getItem(pos); | ||||
|  | ||||
|         Utility.FileType type = Utility.getFileType(ms.name); | ||||
|         if (view instanceof ViewHolderHeader) { | ||||
|             if (item.special == DownloadManager.SPECIAL_NOTHING) return; | ||||
|             int str; | ||||
|             if (item.special == DownloadManager.SPECIAL_PENDING) { | ||||
|                 str = R.string.missions_header_pending; | ||||
|             } else { | ||||
|                 str = R.string.missions_header_finished; | ||||
|                 mClear.setVisible(true); | ||||
|             } | ||||
|  | ||||
|             ((ViewHolderHeader) view).header.setText(str); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         ViewHolderItem h = (ViewHolderItem) view; | ||||
|         h.item = item; | ||||
|  | ||||
|         Utility.FileType type = Utility.getFileType(item.mission.kind, item.mission.name); | ||||
|  | ||||
|         h.icon.setImageResource(Utility.getIconForFileType(type)); | ||||
|         h.name.setText(ms.name); | ||||
|         h.size.setText(Utility.formatBytes(ms.length)); | ||||
|         h.name.setText(item.mission.name); | ||||
|         h.size.setText(Utility.formatBytes(item.mission.length)); | ||||
|  | ||||
|         h.progress = new ProgressDrawable(mContext, Utility.getBackgroundForFileType(type), Utility.getForegroundForFileType(type)); | ||||
|         ViewCompat.setBackground(h.bkg, h.progress); | ||||
|  | ||||
|         h.observer = new MissionObserver(this, h); | ||||
|         ms.addListener(h.observer); | ||||
|         h.progress.setColors(Utility.getBackgroundForFileType(mContext, type), Utility.getForegroundForFileType(mContext, type)); | ||||
|  | ||||
|         if (h.item.mission instanceof DownloadMission) { | ||||
|             DownloadMission mission = (DownloadMission) item.mission; | ||||
|             h.progress.setMarquee(mission.done < 1); | ||||
|             updateProgress(h); | ||||
|             h.pause.setTitle(mission.unknownLength ? R.string.stop : R.string.pause); | ||||
|             mPendingDownloadsItems.add(h); | ||||
|         } else { | ||||
|             h.progress.setMarquee(false); | ||||
|             h.status.setText("100%"); | ||||
|             h.progress.setProgress(1f); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int getItemCount() { | ||||
|         return mItemList.size(); | ||||
|         return mIterator.getOldListSize(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public long getItemId(int position) { | ||||
|         return position; | ||||
|     public int getItemViewType(int position) { | ||||
|         return mIterator.getSpecialAtItem(position); | ||||
|     } | ||||
|  | ||||
|     private void updateProgress(ViewHolder h) { | ||||
|         updateProgress(h, false); | ||||
|     } | ||||
|     private void updateProgress(ViewHolderItem h) { | ||||
|         if (h == null || h.item == null || h.item.mission instanceof FinishedMission) return; | ||||
|  | ||||
|     private void updateProgress(ViewHolder h, boolean finished) { | ||||
|         if (h.mission == null) return; | ||||
|         DownloadMission mission = (DownloadMission) h.item.mission; | ||||
|  | ||||
|         long now = System.currentTimeMillis(); | ||||
|  | ||||
| @@ -164,130 +195,110 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold | ||||
|         } | ||||
|  | ||||
|         if (h.lastDone == -1) { | ||||
|             h.lastDone = h.mission.done; | ||||
|             h.lastDone = mission.done; | ||||
|         } | ||||
|         if (h.lastCurrent != mission.current) { | ||||
|             h.lastCurrent = mission.current; | ||||
|             h.lastDone = 0; | ||||
|             h.lastTimeStamp = now; | ||||
|         } | ||||
|  | ||||
|         long deltaTime = now - h.lastTimeStamp; | ||||
|         long deltaDone = h.mission.done - h.lastDone; | ||||
|         long deltaDone = mission.done - h.lastDone; | ||||
|         boolean hasError = mission.errCode != DownloadMission.ERROR_NOTHING; | ||||
|  | ||||
|         if (deltaTime == 0 || deltaTime > 1000 || finished) { | ||||
|             if (h.mission.errCode > 0) { | ||||
|                 h.status.setText(R.string.msg_error); | ||||
|         if (hasError || deltaTime == 0 || deltaTime > 1000) { | ||||
|             // on error hide marquee or show if condition (mission.done < 1 || mission.unknownLength) is true | ||||
|             h.progress.setMarquee(!hasError && (mission.done < 1 || mission.unknownLength)); | ||||
|  | ||||
|             float progress; | ||||
|             if (mission.unknownLength) { | ||||
|                 progress = Float.NaN; | ||||
|                 h.progress.setProgress(0f); | ||||
|             } else { | ||||
|                 float progress = (float) h.mission.done / h.mission.length; | ||||
|                 h.status.setText(String.format(Locale.US, "%.2f%%", progress * 100)); | ||||
|                 progress = (float) 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); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (hasError) { | ||||
|                 if (Float.isNaN(progress) || Float.isInfinite(progress)) h.progress.setProgress(1f); | ||||
|                 h.status.setText(R.string.msg_error); | ||||
|             } else if (Float.isNaN(progress) || Float.isInfinite(progress)) { | ||||
|                 h.status.setText("--.-%"); | ||||
|             } else { | ||||
|                 h.status.setText(String.format("%.2f%%", progress * 100)); | ||||
|                 h.progress.setProgress(progress); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         long length = mission.offsets[mission.current < mission.offsets.length ? mission.current : (mission.offsets.length - 1)]; | ||||
|         length += mission.length; | ||||
|  | ||||
|         int state = 0; | ||||
|         if (!mission.isFinished()) { | ||||
|             if (!mission.running) { | ||||
|                 state = mission.enqueued ? 1 : 2; | ||||
|             } else if (mission.postprocessingRunning) { | ||||
|                 state = 3; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (state != 0) { | ||||
|             if (h.state != state) { | ||||
|                 String statusStr; | ||||
|                 h.state = state; | ||||
|  | ||||
|                 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; | ||||
|                 } | ||||
|  | ||||
|                 h.size.setText(Utility.formatBytes(length).concat("  (").concat(statusStr).concat(")")); | ||||
|             } else if (deltaTime > 1000 && deltaDone > 0) { | ||||
|                 h.lastTimeStamp = now; | ||||
|                 h.lastDone = mission.done; | ||||
|             } | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (deltaTime > 1000 && deltaDone > 0) { | ||||
|             float speed = (float) deltaDone / deltaTime; | ||||
|             String speedStr = Utility.formatSpeed(speed * 1000); | ||||
|             String sizeStr = Utility.formatBytes(h.mission.length); | ||||
|             String sizeStr = Utility.formatBytes(length); | ||||
|  | ||||
|             h.size.setText(sizeStr + " " + speedStr); | ||||
|             h.size.setText(sizeStr.concat(" ").concat(speedStr)); | ||||
|  | ||||
|             h.lastTimeStamp = now; | ||||
|             h.lastDone = h.mission.done; | ||||
|             h.lastDone = mission.done; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private boolean viewWithFileProvider(@NonNull File file) { | ||||
|         if (!file.exists()) return true; | ||||
|  | ||||
|     private void buildPopup(final ViewHolder h) { | ||||
|         PopupMenu popup = new PopupMenu(mContext, h.menu); | ||||
|         popup.inflate(R.menu.mission); | ||||
|         String ext = Utility.getFileExt(file.getName()); | ||||
|         if (ext == null) return false; | ||||
|  | ||||
|         Menu menu = popup.getMenu(); | ||||
|         MenuItem start = menu.findItem(R.id.start); | ||||
|         MenuItem pause = menu.findItem(R.id.pause); | ||||
|         MenuItem view = menu.findItem(R.id.view); | ||||
|         MenuItem delete = menu.findItem(R.id.delete); | ||||
|         MenuItem checksum = menu.findItem(R.id.checksum); | ||||
|         String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1)); | ||||
|         Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider"); | ||||
|  | ||||
|         // Set to false first | ||||
|         start.setVisible(false); | ||||
|         pause.setVisible(false); | ||||
|         view.setVisible(false); | ||||
|         delete.setVisible(false); | ||||
|         checksum.setVisible(false); | ||||
|  | ||||
|         if (!h.mission.finished) { | ||||
|             if (!h.mission.running) { | ||||
|                 if (h.mission.errCode == -1) { | ||||
|                     start.setVisible(true); | ||||
|                 } | ||||
|  | ||||
|                 delete.setVisible(true); | ||||
|             } else { | ||||
|                 pause.setVisible(true); | ||||
|             } | ||||
|         } else { | ||||
|             view.setVisible(true); | ||||
|             delete.setVisible(true); | ||||
|             checksum.setVisible(true); | ||||
|         } | ||||
|  | ||||
|         popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { | ||||
|             @Override | ||||
|             public boolean onMenuItemClick(MenuItem item) { | ||||
|                 int id = item.getItemId(); | ||||
|                 switch (id) { | ||||
|                     case R.id.start: | ||||
|                         mDownloadManager.resumeMission(h.position); | ||||
|                         mBinder.onMissionAdded(mItemList.get(h.position)); | ||||
|                         return true; | ||||
|                     case R.id.pause: | ||||
|                         mDownloadManager.pauseMission(h.position); | ||||
|                         mBinder.onMissionRemoved(mItemList.get(h.position)); | ||||
|                         h.lastTimeStamp = -1; | ||||
|                         h.lastDone = -1; | ||||
|                         return true; | ||||
|                     case R.id.view: | ||||
|                         File f = new File(h.mission.location, h.mission.name); | ||||
|                         String ext = Utility.getFileExt(h.mission.name); | ||||
|  | ||||
|                         Log.d(TAG, "Viewing file: " + f.getAbsolutePath() + " ext: " + ext); | ||||
|  | ||||
|                         if (ext == null) { | ||||
|                             Log.w(TAG, "Can't view file because it has no extension: " + | ||||
|                                     h.mission.name); | ||||
|                             return false; | ||||
|                         } | ||||
|  | ||||
|                         String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1)); | ||||
|                         Log.v(TAG, "Mime: " + mime + " package: " + mContext.getApplicationContext().getPackageName() + ".provider"); | ||||
|                         if (f.exists()) { | ||||
|                             viewFileWithFileProvider(f, mime); | ||||
|                         } else { | ||||
|                             Log.w(TAG, "File doesn't exist"); | ||||
|                         } | ||||
|  | ||||
|                         return true; | ||||
|                     case R.id.delete: | ||||
|                         mDeleteDownloadManager.add(h.mission); | ||||
|                         updateItemList(); | ||||
|                         notifyDataSetChanged(); | ||||
|                         return true; | ||||
|                     case R.id.md5: | ||||
|                     case R.id.sha1: | ||||
|                         DownloadMission mission = mItemList.get(h.position); | ||||
|                         new ChecksumTask(mContext).execute(mission.location + "/" + mission.name, ALGORITHMS.get(id)); | ||||
|                         return true; | ||||
|                     default: | ||||
|                         return false; | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         popup.show(); | ||||
|     } | ||||
|  | ||||
|     private void viewFileWithFileProvider(File file, String mimetype) { | ||||
|         String ourPackage = mContext.getApplicationContext().getPackageName(); | ||||
|         Uri uri = FileProvider.getUriForFile(mContext, ourPackage + ".provider", file); | ||||
|         Uri uri = FileProvider.getUriForFile(mContext, BuildConfig.APPLICATION_ID + ".provider", file); | ||||
|         Intent intent = new Intent(); | ||||
|         intent.setAction(Intent.ACTION_VIEW); | ||||
|         intent.setDataAndType(uri, mimetype); | ||||
|         intent.setDataAndType(uri, mimeType); | ||||
|         intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION); | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||||
|             intent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION); | ||||
| @@ -300,75 +311,338 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold | ||||
|             Toast noPlayerToast = Toast.makeText(mContext, R.string.toast_no_player, Toast.LENGTH_LONG); | ||||
|             noPlayerToast.show(); | ||||
|         } | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     static class ViewHolder extends RecyclerView.ViewHolder { | ||||
|         public DownloadMission mission; | ||||
|         public int position; | ||||
|     public Handler getMessenger() { | ||||
|         return mHandler; | ||||
|     } | ||||
|  | ||||
|         public final TextView status; | ||||
|         public final ImageView icon; | ||||
|         public final TextView name; | ||||
|         public final TextView size; | ||||
|         public final View bkg; | ||||
|         public final ImageView menu; | ||||
|         public ProgressDrawable progress; | ||||
|         public MissionObserver observer; | ||||
|     private void onServiceMessage(@NonNull Message msg) { | ||||
|         switch (msg.what) { | ||||
|             case DownloadManagerService.MESSAGE_PROGRESS: | ||||
|             case DownloadManagerService.MESSAGE_ERROR: | ||||
|             case DownloadManagerService.MESSAGE_FINISHED: | ||||
|                 break; | ||||
|             default: | ||||
|                 return; | ||||
|         } | ||||
|  | ||||
|         public long lastTimeStamp = -1; | ||||
|         public long lastDone = -1; | ||||
|         public int colorId; | ||||
|         for (int i = 0; i < mPendingDownloadsItems.size(); i++) { | ||||
|             ViewHolderItem h = mPendingDownloadsItems.get(i); | ||||
|             if (h.item.mission != msg.obj) continue; | ||||
|  | ||||
|         public ViewHolder(View v) { | ||||
|             super(v); | ||||
|             if (msg.what == DownloadManagerService.MESSAGE_FINISHED) { | ||||
|                 // DownloadManager should mark the download as finished | ||||
|                 applyChanges(); | ||||
|  | ||||
|             status = v.findViewById(R.id.item_status); | ||||
|             icon = v.findViewById(R.id.item_icon); | ||||
|             name = v.findViewById(R.id.item_name); | ||||
|             size = v.findViewById(R.id.item_size); | ||||
|             bkg = v.findViewById(R.id.item_bkg); | ||||
|             menu = v.findViewById(R.id.item_more); | ||||
|                 mPendingDownloadsItems.remove(i); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             updateProgress(h); | ||||
|             return; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static class MissionObserver implements DownloadMission.MissionListener { | ||||
|         private final MissionAdapter mAdapter; | ||||
|         private final ViewHolder mHolder; | ||||
|     private void showError(@NonNull DownloadMission mission) { | ||||
|         StringBuilder str = new StringBuilder(); | ||||
|         str.append(mContext.getString(R.string.label_code)); | ||||
|         str.append(": "); | ||||
|         str.append(mission.errCode); | ||||
|         str.append('\n'); | ||||
|  | ||||
|         public MissionObserver(MissionAdapter adapter, ViewHolder holder) { | ||||
|             mAdapter = adapter; | ||||
|             mHolder = holder; | ||||
|         switch (mission.errCode) { | ||||
|             case 416: | ||||
|                 str.append(mContext.getString(R.string.error_http_requested_range_not_satisfiable)); | ||||
|                 break; | ||||
|             case 404: | ||||
|                 str.append(mContext.getString(R.string.error_http_not_found)); | ||||
|                 break; | ||||
|             case DownloadMission.ERROR_NOTHING: | ||||
|                 str.append("¿?"); | ||||
|                 break; | ||||
|             case DownloadMission.ERROR_FILE_CREATION: | ||||
|                 str.append(mContext.getString(R.string.error_file_creation)); | ||||
|                 break; | ||||
|             case DownloadMission.ERROR_HTTP_NO_CONTENT: | ||||
|                 str.append(mContext.getString(R.string.error_http_no_content)); | ||||
|                 break; | ||||
|             case DownloadMission.ERROR_HTTP_UNSUPPORTED_RANGE: | ||||
|                 str.append(mContext.getString(R.string.error_http_unsupported_range)); | ||||
|                 break; | ||||
|             case DownloadMission.ERROR_PATH_CREATION: | ||||
|                 str.append(mContext.getString(R.string.error_path_creation)); | ||||
|                 break; | ||||
|             case DownloadMission.ERROR_PERMISSION_DENIED: | ||||
|                 str.append(mContext.getString(R.string.permission_denied)); | ||||
|                 break; | ||||
|             case DownloadMission.ERROR_SSL_EXCEPTION: | ||||
|                 str.append(mContext.getString(R.string.error_ssl_exception)); | ||||
|                 break; | ||||
|             case DownloadMission.ERROR_UNKNOWN_HOST: | ||||
|                 str.append(mContext.getString(R.string.error_unknown_host)); | ||||
|                 break; | ||||
|             case DownloadMission.ERROR_CONNECT_HOST: | ||||
|                 str.append(mContext.getString(R.string.error_connect_host)); | ||||
|                 break; | ||||
|             case DownloadMission.ERROR_POSTPROCESSING_FAILED: | ||||
|                 str.append(R.string.error_postprocessing_failed); | ||||
|             case DownloadMission.ERROR_UNKNOWN_EXCEPTION: | ||||
|                 break; | ||||
|             default: | ||||
|                 if (mission.errCode >= 100 && mission.errCode < 600) { | ||||
|                     str.append("HTTP"); | ||||
|                 } else if (mission.errObject == null) { | ||||
|                     str.append("(not_decelerated_error_code)"); | ||||
|                 } | ||||
|                 break; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onProgressUpdate(DownloadMission downloadMission, long done, long total) { | ||||
|             mAdapter.updateProgress(mHolder); | ||||
|         if (mission.errObject != null) { | ||||
|             str.append("\n\n"); | ||||
|             str.append(mission.errObject.toString()); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onFinish(DownloadMission downloadMission) { | ||||
|             //mAdapter.mManager.deleteMission(mHolder.position); | ||||
|             // TODO Notification | ||||
|             //mAdapter.notifyDataSetChanged(); | ||||
|             if (mHolder.mission != null) { | ||||
|                 mHolder.size.setText(Utility.formatBytes(mHolder.mission.length)); | ||||
|                 mAdapter.updateProgress(mHolder, true); | ||||
|         AlertDialog.Builder builder = new AlertDialog.Builder(mContext); | ||||
|         builder.setTitle(mission.name) | ||||
|                 .setMessage(str) | ||||
|                 .setNegativeButton(android.R.string.ok, (dialog, which) -> dialog.cancel()) | ||||
|                 .create() | ||||
|                 .show(); | ||||
|     } | ||||
|  | ||||
|     public void clearFinishedDownloads() { | ||||
|         mDownloadManager.forgetFinishedDownloads(); | ||||
|         applyChanges(); | ||||
|         mClear.setVisible(false); | ||||
|     } | ||||
|  | ||||
|     private boolean handlePopupItem(@NonNull ViewHolderItem h, @NonNull MenuItem option) { | ||||
|         int id = option.getItemId(); | ||||
|         DownloadMission mission = h.item.mission instanceof DownloadMission ? (DownloadMission) h.item.mission : null; | ||||
|  | ||||
|         if (mission != null) { | ||||
|             switch (id) { | ||||
|                 case R.id.start: | ||||
|                     h.state = -1; | ||||
|                     h.size.setText(Utility.formatBytes(mission.length)); | ||||
|                     mDownloadManager.resumeMission(mission); | ||||
|                     return true; | ||||
|                 case R.id.pause: | ||||
|                     h.state = -1; | ||||
|                     mDownloadManager.pauseMission(mission); | ||||
|                     notifyItemChanged(h.getAdapterPosition()); | ||||
|                     h.lastTimeStamp = -1; | ||||
|                     h.lastDone = -1; | ||||
|                     return true; | ||||
|                 case R.id.error_message_view: | ||||
|                     showError(mission); | ||||
|                     return true; | ||||
|                 case R.id.queue: | ||||
|                     h.queue.setChecked(!h.queue.isChecked()); | ||||
|                     mission.enqueued = h.queue.isChecked(); | ||||
|                     updateProgress(h); | ||||
|                     return true; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onError(DownloadMission downloadMission, int errCode) { | ||||
|             mAdapter.updateProgress(mHolder); | ||||
|         switch (id) { | ||||
|             case R.id.open: | ||||
|                 return viewWithFileProvider(h.item.mission.getDownloadedFile()); | ||||
|             case R.id.delete: | ||||
|                 if (mDeleter == null) { | ||||
|                     mDownloadManager.deleteMission(h.item.mission); | ||||
|                 } else { | ||||
|                     mDeleter.append(h.item.mission); | ||||
|                 } | ||||
|                 applyChanges(); | ||||
|                 return true; | ||||
|             case R.id.md5: | ||||
|             case R.id.sha1: | ||||
|                 new ChecksumTask(mContext).execute(h.item.mission.getDownloadedFile().getAbsolutePath(), ALGORITHMS.get(id)); | ||||
|                 return true; | ||||
|             case R.id.source: | ||||
|                         /*Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(h.item.mission.source)); | ||||
|                         mContext.startActivity(intent);*/ | ||||
|                 try { | ||||
|                     Intent intent = NavigationHelper.getIntentByLink(mContext, h.item.mission.source); | ||||
|                     intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||||
|                     mContext.startActivity(intent); | ||||
|                 } catch (Exception e) { | ||||
|                     Log.w(TAG, "Selected item has a invalid source", e); | ||||
|                 } | ||||
|                 return true; | ||||
|             default: | ||||
|                 return false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void applyChanges() { | ||||
|         mIterator.start(); | ||||
|         DiffUtil.calculateDiff(mIterator, true).dispatchUpdatesTo(this); | ||||
|         mIterator.end(); | ||||
|  | ||||
|         checkEmptyMessageVisibility(); | ||||
|  | ||||
|         if (mIterator.getOldListSize() > 0) { | ||||
|             int lastItemType = mIterator.getSpecialAtItem(mIterator.getOldListSize() - 1); | ||||
|             mClear.setVisible(lastItemType == DownloadManager.SPECIAL_FINISHED); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static class ChecksumTask extends AsyncTask<String, Void, String> { | ||||
|         ProgressDialog prog; | ||||
|         final WeakReference<Activity> weakReference; | ||||
|     public void forceUpdate() { | ||||
|         mIterator.start(); | ||||
|         mIterator.end(); | ||||
|  | ||||
|         ChecksumTask(@NonNull Activity activity) { | ||||
|             weakReference = new WeakReference<>(activity); | ||||
|         notifyDataSetChanged(); | ||||
|     } | ||||
|  | ||||
|     public void setLinear(boolean isLinear) { | ||||
|         mLayout = isLinear ? R.layout.mission_item_linear : R.layout.mission_item; | ||||
|     } | ||||
|  | ||||
|     private void checkEmptyMessageVisibility() { | ||||
|         int flag = mIterator.getOldListSize() > 0 ? View.GONE : View.VISIBLE; | ||||
|         if (mEmptyMessage.getVisibility() != flag) mEmptyMessage.setVisibility(flag); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     public void deleterDispose(Bundle bundle) { | ||||
|         if (mDeleter != null) mDeleter.dispose(bundle); | ||||
|     } | ||||
|  | ||||
|     public void deleterLoad(Bundle bundle, View view) { | ||||
|         if (mDeleter == null) | ||||
|             mDeleter = new Deleter(bundle, view, mContext, this, mDownloadManager, mIterator, mHandler); | ||||
|     } | ||||
|  | ||||
|     public void deleterResume() { | ||||
|         if (mDeleter != null) mDeleter.resume(); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     class ViewHolderItem extends RecyclerView.ViewHolder { | ||||
|         DownloadManager.MissionItem item; | ||||
|  | ||||
|         TextView status; | ||||
|         ImageView icon; | ||||
|         TextView name; | ||||
|         TextView size; | ||||
|         ProgressDrawable progress; | ||||
|  | ||||
|         PopupMenu popupMenu; | ||||
|         MenuItem start; | ||||
|         MenuItem pause; | ||||
|         MenuItem open; | ||||
|         MenuItem queue; | ||||
|         MenuItem showError; | ||||
|         MenuItem delete; | ||||
|         MenuItem source; | ||||
|         MenuItem checksum; | ||||
|  | ||||
|         long lastTimeStamp = -1; | ||||
|         long lastDone = -1; | ||||
|         int lastCurrent = -1; | ||||
|         int state = 0; | ||||
|  | ||||
|         ViewHolderItem(View view) { | ||||
|             super(view); | ||||
|  | ||||
|             progress = new ProgressDrawable(); | ||||
|             ViewCompat.setBackground(itemView.findViewById(R.id.item_bkg), progress); | ||||
|  | ||||
|             status = itemView.findViewById(R.id.item_status); | ||||
|             name = itemView.findViewById(R.id.item_name); | ||||
|             icon = itemView.findViewById(R.id.item_icon); | ||||
|             size = itemView.findViewById(R.id.item_size); | ||||
|  | ||||
|             name.setSelected(true); | ||||
|  | ||||
|             ImageView button = itemView.findViewById(R.id.item_more); | ||||
|             popupMenu = buildPopup(button); | ||||
|             button.setOnClickListener(v -> showPopupMenu()); | ||||
|  | ||||
|             Menu menu = popupMenu.getMenu(); | ||||
|             start = menu.findItem(R.id.start); | ||||
|             pause = menu.findItem(R.id.pause); | ||||
|             open = menu.findItem(R.id.open); | ||||
|             queue = menu.findItem(R.id.queue); | ||||
|             showError = menu.findItem(R.id.error_message_view); | ||||
|             delete = menu.findItem(R.id.delete); | ||||
|             source = menu.findItem(R.id.source); | ||||
|             checksum = menu.findItem(R.id.checksum); | ||||
|  | ||||
|             //h.itemView.setOnClickListener(v -> showDetail(h)); | ||||
|         } | ||||
|  | ||||
|         private void showPopupMenu() { | ||||
|             start.setVisible(false); | ||||
|             pause.setVisible(false); | ||||
|             open.setVisible(false); | ||||
|             queue.setVisible(false); | ||||
|             showError.setVisible(false); | ||||
|             delete.setVisible(false); | ||||
|             source.setVisible(false); | ||||
|             checksum.setVisible(false); | ||||
|  | ||||
|             DownloadMission mission = item.mission instanceof DownloadMission ? (DownloadMission) item.mission : null; | ||||
|  | ||||
|             if (mission != null) { | ||||
|                 if (!mission.postprocessingRunning) { | ||||
|                     if (mission.running) { | ||||
|                         pause.setVisible(true); | ||||
|                     } else { | ||||
|                         if (mission.errCode != DownloadMission.ERROR_NOTHING) { | ||||
|                             showError.setVisible(true); | ||||
|                         } | ||||
|  | ||||
|                         queue.setChecked(mission.enqueued); | ||||
|  | ||||
|                         start.setVisible(mission.errCode != DownloadMission.ERROR_POSTPROCESSING_FAILED); | ||||
|                         delete.setVisible(true); | ||||
|                         queue.setVisible(true); | ||||
|                     } | ||||
|                 } | ||||
|             } else { | ||||
|                 open.setVisible(true); | ||||
|                 delete.setVisible(true); | ||||
|                 checksum.setVisible(true); | ||||
|             } | ||||
|  | ||||
|             if (item.mission.source != null && !item.mission.source.isEmpty()) { | ||||
|                 source.setVisible(true); | ||||
|             } | ||||
|  | ||||
|             popupMenu.show(); | ||||
|         } | ||||
|  | ||||
|         private PopupMenu buildPopup(final View button) { | ||||
|             PopupMenu popup = new PopupMenu(mContext, button); | ||||
|             popup.inflate(R.menu.mission); | ||||
|             popup.setOnMenuItemClickListener(option -> handlePopupItem(this, option)); | ||||
|  | ||||
|             return popup; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     class ViewHolderHeader extends RecyclerView.ViewHolder { | ||||
|         TextView header; | ||||
|  | ||||
|         ViewHolderHeader(View view) { | ||||
|             super(view); | ||||
|             header = itemView.findViewById(R.id.item_name); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     static class ChecksumTask extends AsyncTask<String, Void, String> { | ||||
|         ProgressDialog progressDialog; | ||||
|         WeakReference<Activity> weakReference; | ||||
|  | ||||
|         ChecksumTask(@NonNull Context context) { | ||||
|             weakReference = new WeakReference<>((Activity) context); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
| @@ -378,10 +652,10 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold | ||||
|             Activity activity = getActivity(); | ||||
|             if (activity != null) { | ||||
|                 // Create dialog | ||||
|                 prog = new ProgressDialog(activity); | ||||
|                 prog.setCancelable(false); | ||||
|                 prog.setMessage(activity.getString(R.string.msg_wait)); | ||||
|                 prog.show(); | ||||
|                 progressDialog = new ProgressDialog(activity); | ||||
|                 progressDialog.setCancelable(false); | ||||
|                 progressDialog.setMessage(activity.getString(R.string.msg_wait)); | ||||
|                 progressDialog.show(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -394,10 +668,10 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold | ||||
|         protected void onPostExecute(String result) { | ||||
|             super.onPostExecute(result); | ||||
|  | ||||
|             if (prog != null) { | ||||
|                 Utility.copyToClipboard(prog.getContext(), result); | ||||
|             if (progressDialog != null) { | ||||
|                 Utility.copyToClipboard(progressDialog.getContext(), result); | ||||
|                 if (getActivity() != null) { | ||||
|                     prog.dismiss(); | ||||
|                     progressDialog.dismiss(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| @@ -413,4 +687,5 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
							
								
								
									
										169
									
								
								app/src/main/java/us/shandian/giga/ui/common/Deleter.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								app/src/main/java/us/shandian/giga/ui/common/Deleter.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,169 @@ | ||||
| package us.shandian.giga.ui.common; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.graphics.Color; | ||||
| import android.os.Bundle; | ||||
| import android.os.Handler; | ||||
| import android.support.design.widget.Snackbar; | ||||
| import android.view.View; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
|  | ||||
| import us.shandian.giga.get.Mission; | ||||
| import us.shandian.giga.service.DownloadManager; | ||||
| import us.shandian.giga.service.DownloadManager.MissionIterator; | ||||
| import us.shandian.giga.ui.adapter.MissionAdapter; | ||||
|  | ||||
| public class Deleter { | ||||
|     private static final int TIMEOUT = 5000;// ms | ||||
|     private static final int DELAY = 350;// ms | ||||
|     private static final String BUNDLE_NAMES = "us.shandian.giga.ui.common.deleter.names"; | ||||
|     private static final String BUNDLE_LOCATIONS = "us.shandian.giga.ui.common.deleter.locations"; | ||||
|  | ||||
|     private Snackbar snackbar; | ||||
|     private ArrayList<Mission> items; | ||||
|     private boolean running = true; | ||||
|  | ||||
|     private Context mContext; | ||||
|     private MissionAdapter mAdapter; | ||||
|     private DownloadManager mDownloadManager; | ||||
|     private MissionIterator mIterator; | ||||
|     private Handler mHandler; | ||||
|     private View mView; | ||||
|  | ||||
|     private final Runnable rShow; | ||||
|     private final Runnable rNext; | ||||
|     private final Runnable rCommit; | ||||
|  | ||||
|     public Deleter(Bundle b, View v, Context c, MissionAdapter a, DownloadManager d, MissionIterator i, Handler h) { | ||||
|         mView = v; | ||||
|         mContext = c; | ||||
|         mAdapter = a; | ||||
|         mDownloadManager = d; | ||||
|         mIterator = i; | ||||
|         mHandler = h; | ||||
|  | ||||
|         // use variables to know the reference of the lambdas | ||||
|         rShow = this::show; | ||||
|         rNext = this::next; | ||||
|         rCommit = this::commit; | ||||
|  | ||||
|         items = new ArrayList<>(2); | ||||
|  | ||||
|         if (b != null) { | ||||
|             String[] names = b.getStringArray(BUNDLE_NAMES); | ||||
|             String[] locations = b.getStringArray(BUNDLE_LOCATIONS); | ||||
|  | ||||
|             if (names == null || locations == null) return; | ||||
|             if (names.length < 1 || locations.length < 1) return; | ||||
|             if (names.length != locations.length) return; | ||||
|  | ||||
|             items.ensureCapacity(names.length); | ||||
|  | ||||
|             for (int j = 0; j < locations.length; j++) { | ||||
|                 Mission mission = mDownloadManager.getAnyMission(locations[j], names[j]); | ||||
|                 if (mission == null) continue; | ||||
|  | ||||
|                 items.add(mission); | ||||
|                 mIterator.hide(mission); | ||||
|             } | ||||
|  | ||||
|             if (items.size() > 0) resume(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void append(Mission item) { | ||||
|         mIterator.hide(item); | ||||
|         items.add(0, item); | ||||
|  | ||||
|         show(); | ||||
|     } | ||||
|  | ||||
|     private void forget() { | ||||
|         mIterator.unHide(items.remove(0)); | ||||
|         mAdapter.applyChanges(); | ||||
|  | ||||
|         show(); | ||||
|     } | ||||
|  | ||||
|     private void show() { | ||||
|         if (items.size() < 1) return; | ||||
|  | ||||
|         pause(); | ||||
|         running = true; | ||||
|  | ||||
|         mHandler.postDelayed(rNext, DELAY); | ||||
|     } | ||||
|  | ||||
|     private void next() { | ||||
|         if (items.size() < 1) return; | ||||
|  | ||||
|         String msg = mContext.getString(R.string.file_deleted).concat(":\n").concat(items.get(0).name); | ||||
|  | ||||
|         snackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE); | ||||
|         snackbar.setAction(R.string.undo, s -> forget()); | ||||
|         snackbar.setActionTextColor(Color.YELLOW); | ||||
|         snackbar.show(); | ||||
|  | ||||
|         mHandler.postDelayed(rCommit, TIMEOUT); | ||||
|     } | ||||
|  | ||||
|     private void commit() { | ||||
|         if (items.size() < 1) return; | ||||
|  | ||||
|         while (items.size() > 0) { | ||||
|             Mission mission = items.remove(0); | ||||
|             if (mission.deleted) continue; | ||||
|  | ||||
|             mIterator.unHide(mission); | ||||
|             mDownloadManager.deleteMission(mission); | ||||
|             break; | ||||
|         } | ||||
|  | ||||
|         if (items.size() < 1) { | ||||
|             pause(); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         show(); | ||||
|     } | ||||
|  | ||||
|     private void pause() { | ||||
|         running = false; | ||||
|         mHandler.removeCallbacks(rNext); | ||||
|         mHandler.removeCallbacks(rShow); | ||||
|         mHandler.removeCallbacks(rCommit); | ||||
|         if (snackbar != null) snackbar.dismiss(); | ||||
|     } | ||||
|  | ||||
|     public void resume() { | ||||
|         if (running) return; | ||||
|         mHandler.postDelayed(rShow, (int) (DELAY * 1.5f));// 150% of the delay | ||||
|     } | ||||
|  | ||||
|     public void dispose(Bundle bundle) { | ||||
|         if (items.size() < 1) return; | ||||
|  | ||||
|         pause(); | ||||
|  | ||||
|         if (bundle == null) { | ||||
|             for (Mission mission : items) mDownloadManager.deleteMission(mission); | ||||
|             items = null; | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         String[] names = new String[items.size()]; | ||||
|         String[] locations = new String[items.size()]; | ||||
|  | ||||
|         for (int i = 0; i < items.size(); i++) { | ||||
|             Mission mission = items.get(i); | ||||
|             names[i] = mission.name; | ||||
|             locations[i] = mission.location; | ||||
|         } | ||||
|  | ||||
|         bundle.putStringArray(BUNDLE_NAMES, names); | ||||
|         bundle.putStringArray(BUNDLE_LOCATIONS, locations); | ||||
|     } | ||||
| } | ||||
| @@ -1,25 +1,36 @@ | ||||
| package us.shandian.giga.ui.common; | ||||
| package us.shandian.giga.ui.common;// TODO: ¡git it! | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.graphics.Canvas; | ||||
| import android.graphics.ColorFilter; | ||||
| import android.graphics.Paint; | ||||
| import android.graphics.Path; | ||||
| import android.graphics.PixelFormat; | ||||
| import android.graphics.Rect; | ||||
| import android.graphics.drawable.Drawable; | ||||
| import android.support.annotation.ColorRes; | ||||
| import android.os.Handler; | ||||
| import android.os.Looper; | ||||
| import android.support.annotation.ColorInt; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.v4.content.ContextCompat; | ||||
|  | ||||
| public class ProgressDrawable extends Drawable { | ||||
|     private float mProgress; | ||||
|     private final int mBackgroundColor; | ||||
|     private final int mForegroundColor; | ||||
|     private static final int MARQUEE_INTERVAL = 150; | ||||
|  | ||||
|     public ProgressDrawable(Context context, @ColorRes int background, @ColorRes int foreground) { | ||||
|         this(ContextCompat.getColor(context, background), ContextCompat.getColor(context, foreground)); | ||||
|     private float mProgress; | ||||
|     private int mBackgroundColor, mForegroundColor; | ||||
|     private Handler mMarqueeHandler; | ||||
|     private float mMarqueeProgress; | ||||
|     private Path mMarqueeLine; | ||||
|     private int mMarqueeSize; | ||||
|     private long mMarqueeNext; | ||||
|  | ||||
|     public ProgressDrawable() { | ||||
|         mMarqueeLine = null;// marquee disabled | ||||
|         mMarqueeProgress = 0f; | ||||
|         mMarqueeSize = 0; | ||||
|         mMarqueeNext = 0; | ||||
|     } | ||||
|  | ||||
|     public ProgressDrawable(int background, int foreground) { | ||||
|     public void setColors(@ColorInt int background, @ColorInt int foreground) { | ||||
|         mBackgroundColor = background; | ||||
|         mForegroundColor = foreground; | ||||
|     } | ||||
| @@ -29,10 +40,20 @@ public class ProgressDrawable extends Drawable { | ||||
|         invalidateSelf(); | ||||
|     } | ||||
|  | ||||
|     public void setMarquee(boolean marquee) { | ||||
|         if (marquee == (mMarqueeLine != null)) { | ||||
|             return; | ||||
|         } | ||||
|         mMarqueeLine = marquee ? new Path() : null; | ||||
|         mMarqueeHandler = marquee ? new Handler(Looper.getMainLooper()) : null; | ||||
|         mMarqueeSize = 0; | ||||
|         mMarqueeNext = 0; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void draw(@NonNull Canvas canvas) { | ||||
|         int width = canvas.getWidth(); | ||||
|         int height = canvas.getHeight(); | ||||
|         int width = getBounds().width(); | ||||
|         int height = getBounds().height(); | ||||
|  | ||||
|         Paint paint = new Paint(); | ||||
|  | ||||
| @@ -40,6 +61,42 @@ public class ProgressDrawable extends Drawable { | ||||
|         canvas.drawRect(0, 0, width, height, paint); | ||||
|  | ||||
|         paint.setColor(mForegroundColor); | ||||
|  | ||||
|         if (mMarqueeLine != null) { | ||||
|             if (mMarqueeSize < 1) setupMarquee(width, height); | ||||
|  | ||||
|             int size = mMarqueeSize; | ||||
|             Paint paint2 = new Paint(); | ||||
|             paint2.setColor(mForegroundColor); | ||||
|             paint2.setStrokeWidth(size); | ||||
|             paint2.setStyle(Paint.Style.STROKE); | ||||
|  | ||||
|             size *= 2; | ||||
|  | ||||
|             if (mMarqueeProgress >= size) { | ||||
|                 mMarqueeProgress = 1; | ||||
|             } else { | ||||
|                 mMarqueeProgress++; | ||||
|             } | ||||
|  | ||||
|             // render marquee | ||||
|             width += size * 2; | ||||
|             Path marquee = new Path(); | ||||
|             for (float i = -size; i < width; i += size) { | ||||
|                 marquee.addPath(mMarqueeLine, i + mMarqueeProgress, 0); | ||||
|             } | ||||
|             marquee.close(); | ||||
|  | ||||
|             canvas.drawPath(marquee, paint2);// draw marquee | ||||
|  | ||||
|             if (System.currentTimeMillis() >= mMarqueeNext) { | ||||
|                 // program next update | ||||
|                 mMarqueeNext = System.currentTimeMillis() + MARQUEE_INTERVAL; | ||||
|                 mMarqueeHandler.postDelayed(this::invalidateSelf, MARQUEE_INTERVAL); | ||||
|             } | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         canvas.drawRect(0, 0, (int) (mProgress * width), height, paint); | ||||
|     } | ||||
|  | ||||
| @@ -58,4 +115,17 @@ public class ProgressDrawable extends Drawable { | ||||
|         return PixelFormat.OPAQUE; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onBoundsChange(Rect rect) { | ||||
|         if (mMarqueeLine != null) setupMarquee(rect.width(), rect.height()); | ||||
|     } | ||||
|  | ||||
|     private void setupMarquee(int width, int height) { | ||||
|         mMarqueeSize = (int) ((width * 10f) / 100f);// the size is 10% of the width | ||||
|  | ||||
|         mMarqueeLine.rewind(); | ||||
|         mMarqueeLine.moveTo(-mMarqueeSize, -mMarqueeSize); | ||||
|         mMarqueeLine.lineTo(-mMarqueeSize * 4, height + mMarqueeSize); | ||||
|         mMarqueeLine.close(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -10,8 +10,6 @@ import android.content.SharedPreferences; | ||||
| import android.os.Bundle; | ||||
| import android.os.IBinder; | ||||
| import android.preference.PreferenceManager; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.v7.widget.GridLayoutManager; | ||||
| import android.support.v7.widget.LinearLayoutManager; | ||||
| import android.support.v7.widget.RecyclerView; | ||||
| @@ -23,40 +21,48 @@ import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.download.DeleteDownloadManager; | ||||
|  | ||||
| import io.reactivex.disposables.Disposable; | ||||
| import us.shandian.giga.get.DownloadManager; | ||||
| import us.shandian.giga.service.DownloadManager; | ||||
| import us.shandian.giga.service.DownloadManagerService; | ||||
| import us.shandian.giga.service.DownloadManagerService.DMBinder; | ||||
| import us.shandian.giga.ui.adapter.MissionAdapter; | ||||
|  | ||||
| public abstract class MissionsFragment extends Fragment { | ||||
|     private DownloadManager mDownloadManager; | ||||
|     private DownloadManagerService.DMBinder mBinder; | ||||
| public class MissionsFragment extends Fragment { | ||||
|  | ||||
|     private static final int SPAN_SIZE = 2; | ||||
|  | ||||
|     private SharedPreferences mPrefs; | ||||
|     private boolean mLinear; | ||||
|     private MenuItem mSwitch; | ||||
|     private MenuItem mClear; | ||||
|  | ||||
|     private RecyclerView mList; | ||||
|     private View mEmpty; | ||||
|     private MissionAdapter mAdapter; | ||||
|     private GridLayoutManager mGridManager; | ||||
|     private LinearLayoutManager mLinearManager; | ||||
|     private Context mActivity; | ||||
|     private DeleteDownloadManager mDeleteDownloadManager; | ||||
|     private Disposable mDeleteDisposable; | ||||
|  | ||||
|     private DMBinder mBinder; | ||||
|     private Bundle mBundle; | ||||
|     private boolean mForceUpdate; | ||||
|  | ||||
|     private final ServiceConnection mConnection = new ServiceConnection() { | ||||
|  | ||||
|         @Override | ||||
|         public void onServiceConnected(ComponentName name, IBinder binder) { | ||||
|             mBinder = (DownloadManagerService.DMBinder) binder; | ||||
|             mDownloadManager = setupDownloadManager(mBinder); | ||||
|             if (mDeleteDownloadManager != null) { | ||||
|                 mDeleteDownloadManager.setDownloadManager(mDownloadManager); | ||||
|             mBinder.resetFinishedDownloadCount(); | ||||
|  | ||||
|             mAdapter = new MissionAdapter(mActivity, mBinder.getDownloadManager(), mClear, mEmpty); | ||||
|             mAdapter.deleterLoad(mBundle, getView()); | ||||
|  | ||||
|             mBundle = null; | ||||
|  | ||||
|             mBinder.addMissionEventListener(mAdapter.getMessenger()); | ||||
|  | ||||
|             updateList(); | ||||
|         } | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onServiceDisconnected(ComponentName name) { | ||||
| @@ -66,14 +72,6 @@ public abstract class MissionsFragment extends Fragment { | ||||
|  | ||||
|     }; | ||||
|  | ||||
|     public void setDeleteManager(@NonNull DeleteDownloadManager deleteDownloadManager) { | ||||
|         mDeleteDownloadManager = deleteDownloadManager; | ||||
|         if (mDownloadManager != null) { | ||||
|             mDeleteDownloadManager.setDownloadManager(mDownloadManager); | ||||
|             updateList(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { | ||||
|         View v = inflater.inflate(R.layout.missions, container, false); | ||||
| @@ -81,24 +79,47 @@ public abstract class MissionsFragment extends Fragment { | ||||
|         mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); | ||||
|         mLinear = mPrefs.getBoolean("linear", false); | ||||
|  | ||||
|         mActivity = getActivity(); | ||||
|         mBundle = savedInstanceState; | ||||
|  | ||||
|         // Bind the service | ||||
|         Intent i = new Intent(); | ||||
|         i.setClass(getActivity(), DownloadManagerService.class); | ||||
|         getActivity().bindService(i, mConnection, Context.BIND_AUTO_CREATE); | ||||
|         mActivity.bindService(new Intent(mActivity, DownloadManagerService.class), mConnection, Context.BIND_AUTO_CREATE); | ||||
|  | ||||
|         // Views | ||||
|         mEmpty = v.findViewById(R.id.list_empty_view); | ||||
|         mList = v.findViewById(R.id.mission_recycler); | ||||
|  | ||||
|         // Init | ||||
|         mGridManager = new GridLayoutManager(getActivity(), 2); | ||||
|         mGridManager = new GridLayoutManager(getActivity(), SPAN_SIZE); | ||||
|         mGridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { | ||||
|             @Override | ||||
|             public int getSpanSize(int position) { | ||||
|                 switch (mAdapter.getItemViewType(position)) { | ||||
|                     case DownloadManager.SPECIAL_PENDING: | ||||
|                     case DownloadManager.SPECIAL_FINISHED: | ||||
|                         return SPAN_SIZE; | ||||
|                     default: | ||||
|                         return 1; | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         mLinearManager = new LinearLayoutManager(getActivity()); | ||||
|         mList.setLayoutManager(mGridManager); | ||||
|  | ||||
|         setHasOptionsMenu(true); | ||||
|  | ||||
|         return v; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { | ||||
|         super.onCreateOptionsMenu(menu, inflater); | ||||
|         if (menu != null) { | ||||
|             mSwitch = menu.findItem(R.id.switch_mode); | ||||
|             mClear = menu.findItem(R.id.clear_list); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Added in API level 23. | ||||
|      */ | ||||
| @@ -108,7 +129,7 @@ public abstract class MissionsFragment extends Fragment { | ||||
|  | ||||
|         // Bug: in api< 23 this is never called | ||||
|         // so mActivity=null | ||||
|         // so app crashes with nullpointer exception | ||||
|         // so app crashes with null-pointer exception | ||||
|         mActivity = activity; | ||||
|     } | ||||
|  | ||||
| @@ -119,36 +140,45 @@ public abstract class MissionsFragment extends Fragment { | ||||
|     @Override | ||||
|     public void onAttach(Activity activity) { | ||||
|         super.onAttach(activity); | ||||
|  | ||||
|         mActivity = activity; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { | ||||
|         super.onViewCreated(view, savedInstanceState); | ||||
|         if (mDeleteDownloadManager != null) { | ||||
|             mDeleteDisposable = mDeleteDownloadManager.getUndoObservable().subscribe(mission -> { | ||||
|     public void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|         if (mBinder == null || mAdapter == null) return; | ||||
|  | ||||
|         mBinder.removeMissionEventListener(mAdapter.getMessenger()); | ||||
|         mActivity.unbindService(mConnection); | ||||
|         mAdapter.deleterDispose(null); | ||||
|  | ||||
|         mBinder = null; | ||||
|         mAdapter = null; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onSaveInstanceState(Bundle outState) { | ||||
|         super.onSaveInstanceState(outState); | ||||
|         if (mAdapter != null) { | ||||
|                     mAdapter.updateItemList(); | ||||
|                     mAdapter.notifyDataSetChanged(); | ||||
|                 } | ||||
|             }); | ||||
|             mAdapter.deleterDispose(outState); | ||||
|             mForceUpdate = true; | ||||
|             mBinder.removeMissionEventListener(mAdapter.getMessenger()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onDestroyView() { | ||||
|         super.onDestroyView(); | ||||
|         getActivity().unbindService(mConnection); | ||||
|         if (mDeleteDisposable != null) { | ||||
|             mDeleteDisposable.dispose(); | ||||
|         } | ||||
|     public void onResume() { | ||||
|         super.onResume(); | ||||
|         if (mAdapter != null) { | ||||
|             mAdapter.deleterResume(); | ||||
|  | ||||
|             if (mForceUpdate) { | ||||
|                 mForceUpdate = false; | ||||
|                 mAdapter.forceUpdate(); | ||||
|             } | ||||
|  | ||||
|     @Override | ||||
|     public void onPrepareOptionsMenu(Menu menu) { | ||||
|         mSwitch = menu.findItem(R.id.switch_mode); | ||||
|         super.onPrepareOptionsMenu(menu); | ||||
|             mBinder.addMissionEventListener(mAdapter.getMessenger()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -158,32 +188,30 @@ public abstract class MissionsFragment extends Fragment { | ||||
|                 mLinear = !mLinear; | ||||
|                 updateList(); | ||||
|                 return true; | ||||
|             case R.id.clear_list: | ||||
|                 mAdapter.clearFinishedDownloads(); | ||||
|                 return true; | ||||
|             default: | ||||
|                 return super.onOptionsItemSelected(item); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void notifyChange() { | ||||
|         mAdapter.notifyDataSetChanged(); | ||||
|     } | ||||
|  | ||||
|     private void updateList() { | ||||
|         mAdapter = new MissionAdapter((Activity) mActivity, mBinder, mDownloadManager, mDeleteDownloadManager, mLinear); | ||||
|  | ||||
|         if (mLinear) { | ||||
|             mList.setLayoutManager(mLinearManager); | ||||
|         } else { | ||||
|             mList.setLayoutManager(mGridManager); | ||||
|         } | ||||
|  | ||||
|         mList.setAdapter(null); | ||||
|         mAdapter.notifyDataSetChanged(); | ||||
|         mAdapter.setLinear(mLinear); | ||||
|         mList.setAdapter(mAdapter); | ||||
|  | ||||
|         if (mSwitch != null) { | ||||
|             mSwitch.setIcon(mLinear ? R.drawable.grid : R.drawable.list); | ||||
|         } | ||||
|  | ||||
|             mSwitch.setTitle(mLinear ? R.string.grid : R.string.list); | ||||
|             mPrefs.edit().putBoolean("linear", mLinear).apply(); | ||||
|         } | ||||
|  | ||||
|     protected abstract DownloadManager setupDownloadManager(DownloadManagerService.DMBinder binder); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -3,10 +3,11 @@ package us.shandian.giga.util; | ||||
| import android.content.ClipData; | ||||
| import android.content.ClipboardManager; | ||||
| import android.content.Context; | ||||
| import android.support.annotation.ColorRes; | ||||
| import android.support.annotation.ColorInt; | ||||
| import android.support.annotation.DrawableRes; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.v4.content.ContextCompat; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| @@ -21,12 +22,14 @@ import java.io.ObjectOutputStream; | ||||
| import java.io.Serializable; | ||||
| import java.security.MessageDigest; | ||||
| import java.security.NoSuchAlgorithmException; | ||||
| import java.util.Locale; | ||||
|  | ||||
| public class Utility { | ||||
|  | ||||
|     public enum FileType { | ||||
|         VIDEO, | ||||
|         MUSIC, | ||||
|         SUBTITLE, | ||||
|         UNKNOWN | ||||
|     } | ||||
|  | ||||
| @@ -54,36 +57,27 @@ public class Utility { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static void writeToFile(@NonNull String fileName, @NonNull Serializable serializable) { | ||||
|         ObjectOutputStream objectOutputStream = null; | ||||
|     public static void writeToFile(@NonNull File file, @NonNull Serializable serializable) { | ||||
|  | ||||
|         try { | ||||
|             objectOutputStream = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(fileName))); | ||||
|         try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(file)))) { | ||||
|             objectOutputStream.writeObject(serializable); | ||||
|         } catch (Exception e) { | ||||
|             //nothing to do | ||||
|         } finally { | ||||
|             if(objectOutputStream != null) { | ||||
|                 try { | ||||
|                     objectOutputStream.close(); | ||||
|                 } catch (Exception e) { | ||||
|         } | ||||
|         //nothing to do | ||||
|     } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Nullable | ||||
|     @SuppressWarnings("unchecked") | ||||
|     public static <T> T readFromFile(String file) { | ||||
|         T object = null; | ||||
|     public static <T> T readFromFile(File file) { | ||||
|         T object; | ||||
|         ObjectInputStream objectInputStream = null; | ||||
|  | ||||
|         try { | ||||
|             objectInputStream = new ObjectInputStream(new FileInputStream(file)); | ||||
|             object = (T) objectInputStream.readObject(); | ||||
|         } catch (Exception e) { | ||||
|             //nothing to do | ||||
|             object = null; | ||||
|         } | ||||
|  | ||||
|         if (objectInputStream != null) { | ||||
| @@ -119,39 +113,68 @@ public class Utility { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static FileType getFileType(String file) { | ||||
|         if (file.endsWith(".mp3") || file.endsWith(".wav") || file.endsWith(".flac") || file.endsWith(".m4a")) { | ||||
|     public static FileType getFileType(char kind, String file) { | ||||
|         switch (kind) { | ||||
|             case 'v': | ||||
|                 return FileType.VIDEO; | ||||
|             case 'a': | ||||
|                 return FileType.MUSIC; | ||||
|             case 's': | ||||
|                 return FileType.SUBTITLE; | ||||
|             //default '?': | ||||
|         } | ||||
|  | ||||
|         if (file.endsWith(".srt") || file.endsWith(".vtt") || file.endsWith(".ssa")) { | ||||
|             return FileType.SUBTITLE; | ||||
|         } else if (file.endsWith(".mp3") || file.endsWith(".wav") || file.endsWith(".flac") || file.endsWith(".m4a") || file.endsWith(".opus")) { | ||||
|             return FileType.MUSIC; | ||||
|         } else if (file.endsWith(".mp4") || file.endsWith(".mpeg") || file.endsWith(".rm") || file.endsWith(".rmvb") | ||||
|                 || file.endsWith(".flv") || file.endsWith(".webp") || file.endsWith(".webm")) { | ||||
|             return FileType.VIDEO; | ||||
|         } else { | ||||
|         } | ||||
|  | ||||
|         return FileType.UNKNOWN; | ||||
|     } | ||||
|     } | ||||
|  | ||||
|     @ColorRes | ||||
|     public static int getBackgroundForFileType(FileType type) { | ||||
|     @ColorInt | ||||
|     public static int getBackgroundForFileType(Context ctx, FileType type) { | ||||
|         int colorRes; | ||||
|         switch (type) { | ||||
|             case MUSIC: | ||||
|                 return R.color.audio_left_to_load_color; | ||||
|                 colorRes = R.color.audio_left_to_load_color; | ||||
|                 break; | ||||
|             case VIDEO: | ||||
|                 return R.color.video_left_to_load_color; | ||||
|                 colorRes = R.color.video_left_to_load_color; | ||||
|                 break; | ||||
|             case SUBTITLE: | ||||
|                 colorRes = R.color.subtitle_left_to_load_color; | ||||
|                 break; | ||||
|             default: | ||||
|                 return R.color.gray; | ||||
|         } | ||||
|                 colorRes = R.color.gray; | ||||
|         } | ||||
|  | ||||
|     @ColorRes | ||||
|     public static int getForegroundForFileType(FileType type) { | ||||
|         return ContextCompat.getColor(ctx, colorRes); | ||||
|     } | ||||
|  | ||||
|     @ColorInt | ||||
|     public static int getForegroundForFileType(Context ctx, FileType type) { | ||||
|         int colorRes; | ||||
|         switch (type) { | ||||
|             case MUSIC: | ||||
|                 return R.color.audio_already_load_color; | ||||
|                 colorRes = R.color.audio_already_load_color; | ||||
|                 break; | ||||
|             case VIDEO: | ||||
|                 return R.color.video_already_load_color; | ||||
|                 colorRes = R.color.video_already_load_color; | ||||
|                 break; | ||||
|             case SUBTITLE: | ||||
|                 colorRes = R.color.subtitle_already_load_color; | ||||
|                 break; | ||||
|             default: | ||||
|                 return R.color.gray; | ||||
|                 colorRes = R.color.gray; | ||||
|                 break; | ||||
|         } | ||||
|  | ||||
|         return ContextCompat.getColor(ctx, colorRes); | ||||
|     } | ||||
|  | ||||
|     @DrawableRes | ||||
| @@ -161,6 +184,8 @@ public class Utility { | ||||
|                 return R.drawable.music; | ||||
|             case VIDEO: | ||||
|                 return R.drawable.video; | ||||
|             case SUBTITLE: | ||||
|                 return R.drawable.subtitle; | ||||
|             default: | ||||
|                 return R.drawable.video; | ||||
|         } | ||||
| @@ -168,12 +193,18 @@ public class Utility { | ||||
|  | ||||
|     public static void copyToClipboard(Context context, String str) { | ||||
|         ClipboardManager cm = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); | ||||
|  | ||||
|         if (cm == null) { | ||||
|             Toast.makeText(context, R.string.permission_denied, Toast.LENGTH_LONG).show(); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         cm.setPrimaryClip(ClipData.newPlainText("text", str)); | ||||
|         Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show(); | ||||
|     } | ||||
|  | ||||
|     public static String checksum(String path, String algorithm) { | ||||
|         MessageDigest md = null; | ||||
|         MessageDigest md; | ||||
|  | ||||
|         try { | ||||
|             md = MessageDigest.getInstance(algorithm); | ||||
| @@ -181,7 +212,7 @@ public class Utility { | ||||
|             throw new RuntimeException(e); | ||||
|         } | ||||
|  | ||||
|         FileInputStream i = null; | ||||
|         FileInputStream i; | ||||
|  | ||||
|         try { | ||||
|             i = new FileInputStream(path); | ||||
| @@ -190,14 +221,14 @@ public class Utility { | ||||
|         } | ||||
|  | ||||
|         byte[] buf = new byte[1024]; | ||||
|         int len = 0; | ||||
|         int len; | ||||
|  | ||||
|         try { | ||||
|             while ((len = i.read(buf)) != -1) { | ||||
|                 md.update(buf, 0, len); | ||||
|             } | ||||
|         } catch (IOException ignored) { | ||||
|  | ||||
|         } catch (IOException e) { | ||||
|             // nothing to do | ||||
|         } | ||||
|  | ||||
|         byte[] digest = md.digest(); | ||||
| @@ -211,4 +242,16 @@ public class Utility { | ||||
|         return sb.toString(); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     @SuppressWarnings("ResultOfMethodCallIgnored") | ||||
|     public static boolean mkdir(File path, boolean allDirs) { | ||||
|         if (path.exists()) return true; | ||||
|  | ||||
|         if (allDirs) | ||||
|             path.mkdirs(); | ||||
|         else | ||||
|             path.mkdir(); | ||||
|  | ||||
|         return path.exists(); | ||||
|     } | ||||
| } | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.3 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xhdpi/subtitle.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xhdpi/subtitle.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 3.6 KiB | 
| @@ -53,6 +53,12 @@ | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:text="@string/audio"/> | ||||
|  | ||||
|         <RadioButton | ||||
|             android:id="@+id/subtitle_button" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:text="@string/caption_setting_title"/> | ||||
|     </RadioGroup> | ||||
|  | ||||
|     <Spinner | ||||
| @@ -77,6 +83,7 @@ | ||||
|         android:text="@string/msg_threads"/> | ||||
|  | ||||
|     <LinearLayout | ||||
|         android:id="@+id/threads_layout" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_below="@+id/threads_text_view" | ||||
|   | ||||
| @@ -38,7 +38,7 @@ | ||||
| 				android:layout_centerVertical="true" | ||||
| 				android:layout_marginRight="1dp" | ||||
| 				android:src="@drawable/ic_menu_more" | ||||
| 				android:scaleType="centerInside" | ||||
| 				android:scaleType="center" | ||||
|                 android:contentDescription="TODO"/> | ||||
| 			 | ||||
| 		</RelativeLayout> | ||||
| @@ -51,8 +51,8 @@ | ||||
| 			android:layout_centerHorizontal="true" | ||||
| 			android:scaleType="fitXY" | ||||
| 			android:gravity="center" | ||||
| 			android:padding="10dp" | ||||
|             android:contentDescription="TODO" /> | ||||
|             android:contentDescription="TODO" | ||||
| 			android:padding="10dp"/> | ||||
| 		 | ||||
| 		<TextView | ||||
| 			android:id="@+id/item_name" | ||||
| @@ -60,12 +60,14 @@ | ||||
| 			android:layout_height="wrap_content" | ||||
| 			android:layout_below="@id/item_icon" | ||||
| 			android:padding="6dp" | ||||
| 			android:singleLine="true" | ||||
| 			android:ellipsize="end" | ||||
| 			android:text="XXX.xx" | ||||
| 			android:textSize="16sp" | ||||
| 			android:textStyle="bold" | ||||
| 			android:textColor="@color/white"/> | ||||
| 			android:textColor="@color/white" | ||||
| 			android:singleLine="true" | ||||
| 			android:ellipsize="marquee" | ||||
| 			android:marqueeRepeatLimit="marquee_forever" | ||||
| 			android:scrollHorizontally="true"/> | ||||
| 		 | ||||
| 		<TextView | ||||
| 			android:id="@+id/item_size" | ||||
|   | ||||
| @@ -1,12 +1,17 @@ | ||||
| <LinearLayout | ||||
| 	xmlns:android="http://schemas.android.com/apk/res/android" | ||||
| <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
| 	android:layout_width="fill_parent" | ||||
| 	android:layout_height="fill_parent" | ||||
| 	android:orientation="vertical"> | ||||
|  | ||||
| 	<include | ||||
| 		layout="@layout/list_empty_view" | ||||
| 		android:id="@+id/list_empty_view" | ||||
| 		android:visibility="gone" /> | ||||
|  | ||||
| 	<android.support.v7.widget.RecyclerView | ||||
| 		android:id="@+id/mission_recycler" | ||||
| 		android:layout_width="match_parent" | ||||
| 		android:layout_height="match_parent"/> | ||||
| 		android:layout_height="match_parent" | ||||
| 		/> | ||||
|  | ||||
| </LinearLayout> | ||||
							
								
								
									
										30
									
								
								app/src/main/res/layout/missions_header.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								app/src/main/res/layout/missions_header.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
|  | ||||
| <LinearLayout | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="30dp" | ||||
|     android:layout_marginRight="16dp" | ||||
|     android:layout_marginEnd="16dp" | ||||
|     android:layout_marginTop="16dp" | ||||
|     android:orientation="vertical" | ||||
|     android:layout_marginLeft="8dp" | ||||
|     android:layout_marginStart="8dp"> | ||||
|  | ||||
|  | ||||
|     <TextView | ||||
|         android:id="@+id/item_name" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:gravity="center_vertical" | ||||
|         android:textColor="@color/drawer_header_font_color" | ||||
|         android:textSize="16sp" | ||||
|         android:textStyle="bold" /> | ||||
|  | ||||
|     <View | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="2dp" | ||||
|         android:background="@color/black_settings_accent_color" /> | ||||
|  | ||||
| </LinearLayout> | ||||
| @@ -1,11 +1,25 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <menu xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto"> | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     xmlns:tools="http://schemas.android.com/tools"> | ||||
|  | ||||
|     <item | ||||
|         android:id="@+id/switch_mode" | ||||
|         android:icon="@drawable/list" | ||||
|         android:title="@string/grid" | ||||
|         app:showAsAction="ifRoom" /> | ||||
|  | ||||
|     <item | ||||
|         android:id="@+id/action_settings" | ||||
|         android:title="@string/settings" | ||||
|         app:showAsAction="never" /> | ||||
|  | ||||
|     <item | ||||
|         android:visible="false" | ||||
|         android:id="@+id/clear_list" | ||||
|         android:icon="@drawable/ic_delete_sweep_white_24dp" | ||||
|         android:title="@string/clear_finished_download" | ||||
|         app:showAsAction="ifRoom" /> | ||||
|  | ||||
|  | ||||
|     <item android:id="@+id/action_settings" | ||||
|         app:showAsAction="never" | ||||
|         android:title="@string/settings"/> | ||||
|     <item android:id="@+id/switch_mode" | ||||
|         app:showAsAction="ifRoom" | ||||
|         android:title="@string/switch_view"/> | ||||
| </menu> | ||||
| @@ -9,13 +9,26 @@ | ||||
|         android:title="@string/pause" /> | ||||
|  | ||||
|     <item | ||||
| 		android:id="@+id/view" | ||||
|         android:id="@+id/queue" | ||||
|         android:title="@string/enqueue" | ||||
|         android:checkable="true"/> | ||||
|  | ||||
|     <item | ||||
|         android:id="@+id/open" | ||||
|         android:title="@string/view" /> | ||||
|  | ||||
|     <item | ||||
|         android:id="@+id/delete" | ||||
|         android:title="@string/delete" /> | ||||
|  | ||||
|     <item | ||||
|         android:id="@+id/error_message_view" | ||||
|         android:title="@string/show_error" /> | ||||
|  | ||||
|     <item | ||||
|         android:id="@+id/source" | ||||
|         android:title="@string/show_info" /> | ||||
|  | ||||
|     <item | ||||
|         android:id="@+id/checksum" | ||||
|         android:title="@string/checksum"> | ||||
|   | ||||
| @@ -500,4 +500,54 @@ abrir en modo popup</string> | ||||
|     <string name="users">Usuarios</string> | ||||
|     <string name="playlists">Listas de reproducción</string> | ||||
|     <string name="tracks">Pistas</string> | ||||
|      | ||||
|     <string name="missions_header_finished">Finalizadas</string> | ||||
|     <string name="missions_header_pending">En cola</string> | ||||
|      | ||||
|     <string name="paused">pausado</string> | ||||
|     <string name="queued">en cola</string> | ||||
|     <string name="post_processing">post-procesado</string> | ||||
|      | ||||
|     <string name="enqueue">Encolar</string> | ||||
|      | ||||
|     <string name="permission_denied">Acción denegada por el sistema</string> | ||||
|  | ||||
|     <string name="file_deleted">Archivo borrado</string> | ||||
|  | ||||
|     <!-- download done notifications --> | ||||
|     <string name="download_finished">Descarga finalizada: %s</string> | ||||
|     <string name="download_finished_more">%s descargas finalizadas</string> | ||||
|  | ||||
|     <!-- dialog about existing downloads --> | ||||
|     <string name="generate_unique_name">Generar nombre único</string> | ||||
|     <string name="overwrite">Sobrescribir</string> | ||||
|     <string name="overwrite_warning">Ya existe un archivo descargado con este nombre</string> | ||||
|     <string name="download_already_running">Hay una descarga en curso con este nombre</string> | ||||
|  | ||||
|     <string name="grid">Mostrar como grilla</string> | ||||
|     <string name="list">Mostrar como lista</string> | ||||
|     <string name="clear_finished_download">Limpiar descargas finalizadas</string> | ||||
|     <string name="msg_pending_downloads">Tienes %s descargas pendientes, ve a Descargas para continuarlas</string> | ||||
|     <string name="stop">Detener</string> | ||||
|     <string name="max_retry_msg">Intentos maximos</string> | ||||
|     <string name="max_retry_desc">Cantidad máxima de intentos antes de cancelar la descarga</string> | ||||
|     <string name="pause_downloads_on_mobile">Pausar al cambiar a datos moviles</string> | ||||
|     <string name="pause_downloads_on_mobile_desc">No todas las descargas se pueden suspender, en esos casos, se reiniciaran</string> | ||||
|  | ||||
|  | ||||
|     <!-- message dialog about download error --> | ||||
|     <string name="show_error">Mostrar error</string> | ||||
|     <string name="label_code">Codigo</string> | ||||
|     <string name="error_path_creation">No se puede crear la carpeta de destino</string> | ||||
|     <string name="error_file_creation">No se puede crear el archivo</string> | ||||
|     <string name="error_permission_denied">Permiso denegado por el sistema</string> | ||||
|     <string name="error_ssl_exception">Fallo la conexión segura</string> | ||||
|     <string name="error_unknown_host">No se puede encontrar el servidor</string> | ||||
|     <string name="error_connect_host">No se puede conectar con el servidor</string> | ||||
|     <string name="error_http_no_content">El servidor no devolvio datos</string> | ||||
|     <string name="error_http_unsupported_range">El servidor no acepta descargas multi-hilos, intente de nuevo con @string/msg_threads = 1</string> | ||||
|     <string name="error_http_requested_range_not_satisfiable">Rango solicitado no satisfactorio</string> | ||||
|     <string name="error_http_not_found">No encontrado</string> | ||||
|     <string name="error_postprocessing_failed">Fallo el post-procesado</string> | ||||
|      | ||||
|     </resources> | ||||
|   | ||||
| @@ -63,6 +63,8 @@ | ||||
|     <color name="audio_already_load_color">#000000</color> | ||||
|     <color name="video_left_to_load_color">#CD5656</color> | ||||
|     <color name="video_already_load_color">#BC211D</color> | ||||
|     <color name="subtitle_left_to_load_color">#008ea4</color> | ||||
|     <color name="subtitle_already_load_color">#005a71</color> | ||||
|  | ||||
|     <!-- GigaGet Component colors --> | ||||
|     <color name="white">#FFFFFF</color> | ||||
|   | ||||
| @@ -175,6 +175,12 @@ | ||||
|  | ||||
|     <string name="default_file_charset_value" translatable="false">@string/charset_most_special_characters_value</string> | ||||
|      | ||||
|     <string name="downloads_max_retry" translatable="false">downloads_max_retry</string> | ||||
|     <string name="default_max_retry" translatable="false">3</string> | ||||
|     <string name="cross_network_downloads" translatable="false">cross_network_downloads</string> | ||||
|  | ||||
|     <string name="default_download_threads" translatable="false">default_download_threads</string> | ||||
|  | ||||
|     <!-- Preferred action on open (open from external app) --> | ||||
|     <string name="preferred_open_action_key" translatable="false">preferred_open_action_key</string> | ||||
|     <string name="preferred_open_action_default" translatable="false">@string/always_ask_open_action_key</string> | ||||
|   | ||||
| @@ -16,7 +16,7 @@ | ||||
|     <string name="controls_download_desc">Download stream file</string> | ||||
|     <string name="search">Search</string> | ||||
|     <string name="settings">Settings</string> | ||||
|     <string name="did_you_mean">Did you mean: %1$s\?</string> | ||||
|     <string name="did_you_mean">Did you mean: %1$s?</string> | ||||
|     <string name="share_dialog_title">Share with</string> | ||||
|     <string name="choose_browser">Choose browser</string> | ||||
|     <string name="screen_rotation">rotation</string> | ||||
| @@ -143,6 +143,7 @@ | ||||
|     <string name="popup_resizing_indicator_title">Resizing</string> | ||||
|     <string name="best_resolution">Best resolution</string> | ||||
|     <string name="undo">Undo</string> | ||||
|     <string name="file_deleted">File deleted</string> | ||||
|     <string name="play_all">Play All</string> | ||||
|     <string name="always">Always</string> | ||||
|     <string name="just_once">Just Once</string> | ||||
| @@ -525,4 +526,50 @@ | ||||
|     <string name="grid">Grid</string> | ||||
| 	<string name="auto">Auto</string> | ||||
|     <string name="switch_view">Switch View</string> | ||||
|  | ||||
|  | ||||
|     <string name="missions_header_finished">Finished</string> | ||||
|     <string name="missions_header_pending">In queue</string> | ||||
|      | ||||
|     <string name="paused">paused</string> | ||||
|     <string name="queued">queued</string> | ||||
|     <string name="post_processing">post-processing</string> | ||||
|      | ||||
|     <string name="enqueue">Queue</string> | ||||
|      | ||||
|     <string name="permission_denied">Action denied by the system</string> | ||||
|  | ||||
|     <!-- download done notifications --> | ||||
|     <string name="download_finished">Download finished: %s</string> | ||||
|     <string name="download_finished_more">%s downloads finished</string> | ||||
|  | ||||
|     <!-- dialog about existing downloads --> | ||||
|     <string name="generate_unique_name">Generate unique name</string> | ||||
|     <string name="overwrite">Overwrite</string> | ||||
|     <string name="overwrite_warning">A downloaded file with this name already exists</string> | ||||
|     <string name="download_already_running">There is a download in progress with this name</string> | ||||
|  | ||||
|     <!-- message dialog about download error --> | ||||
|     <string name="show_error">Show error</string> | ||||
|     <string name="label_code">Code</string> | ||||
|     <string name="error_path_creation">The file can not be created</string> | ||||
|     <string name="error_file_creation">The destination folder can not be created</string> | ||||
|     <string name="error_permission_denied">Permission denied by the system</string> | ||||
|     <string name="error_ssl_exception">Secure connection failed</string> | ||||
|     <string name="error_unknown_host">Can not found the server</string> | ||||
|     <string name="error_connect_host">Can not connect to the server</string> | ||||
|     <string name="error_http_no_content">The server does not send data</string> | ||||
|     <string name="error_http_unsupported_range">The server does not accept multi-threaded downloads, retry with @string/msg_threads = 1</string> | ||||
|     <string name="error_http_requested_range_not_satisfiable">Requested Range Not Satisfiable</string> | ||||
|     <string name="error_http_not_found">Not found</string> | ||||
|     <string name="error_postprocessing_failed">Post-processing failed</string> | ||||
|  | ||||
|     <string name="clear_finished_download">Clear finished downloads</string> | ||||
|     <string name="msg_pending_downloads">You have %s pending downloads, goto Downloads to continue</string> | ||||
|     <string name="stop">Stop</string> | ||||
|     <string name="max_retry_msg">Maximum retry</string> | ||||
|     <string name="max_retry_desc">Maximum number of attempts before canceling the download</string> | ||||
|     <string name="pause_downloads_on_mobile">Pause on switching to mobile data</string> | ||||
|     <string name="pause_downloads_on_mobile_desc">Not all downloads can be suspended, in those cases, will be restarted</string> | ||||
|  | ||||
| </resources> | ||||
|   | ||||
| @@ -29,4 +29,17 @@ | ||||
|         android:summary="@string/settings_file_replacement_character_summary" | ||||
|         android:title="@string/settings_file_replacement_character_title"/> | ||||
|  | ||||
|     <SeekBarPreference | ||||
|         android:defaultValue="@string/default_max_retry" | ||||
|         android:key="@string/downloads_max_retry" | ||||
|         android:max="15" | ||||
|         android:summary="@string/max_retry_desc" | ||||
|         android:title="@string/max_retry_msg" /> | ||||
|  | ||||
|     <CheckBoxPreference | ||||
|         android:defaultValue="false" | ||||
|         android:key="@string/cross_network_downloads" | ||||
|         android:summary="@string/pause_downloads_on_mobile_desc" | ||||
|         android:title="@string/pause_downloads_on_mobile" /> | ||||
|  | ||||
| </PreferenceScreen> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 kapodamy
					kapodamy