mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 07:13:00 +00:00 
			
		
		
		
	Implement Storage Access Framework
* re-work finished mission database * re-work DownloadMission and bump it Serializable version * keep the classic Java IO API * SAF Tree API support on Android Lollipop or higher * add wrapper for SAF stream opening * implement Closeable in SharpStream to replace the dispose() method * do required changes for this API: ** remove any file creation logic from DownloadInitializer ** make PostProcessing Serializable and reduce the number of iterations ** update all strings.xml files ** storage helpers: StoredDirectoryHelper & StoredFileHelper ** best effort to handle any kind of SAF errors/exceptions
This commit is contained in:
		| @@ -55,7 +55,7 @@ public class DownloadActivity extends AppCompatActivity { | |||||||
|     private void updateFragments() { |     private void updateFragments() { | ||||||
|         MissionsFragment fragment = new MissionsFragment(); |         MissionsFragment fragment = new MissionsFragment(); | ||||||
|  |  | ||||||
|         getFragmentManager().beginTransaction() |         getSupportFragmentManager().beginTransaction() | ||||||
|                 .replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG) |                 .replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG) | ||||||
|                 .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) |                 .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) | ||||||
|                 .commit(); |                 .commit(); | ||||||
|   | |||||||
| @@ -1,8 +1,14 @@ | |||||||
| package org.schabi.newpipe.download; | package org.schabi.newpipe.download; | ||||||
|  |  | ||||||
|  | import android.app.Activity; | ||||||
|  | import android.content.ComponentName; | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
|  | import android.content.Intent; | ||||||
|  | import android.content.ServiceConnection; | ||||||
| import android.content.SharedPreferences; | import android.content.SharedPreferences; | ||||||
|  | import android.net.Uri; | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
|  | import android.os.IBinder; | ||||||
| import android.preference.PreferenceManager; | import android.preference.PreferenceManager; | ||||||
| import android.support.annotation.IdRes; | import android.support.annotation.IdRes; | ||||||
| import android.support.annotation.NonNull; | import android.support.annotation.NonNull; | ||||||
| @@ -14,6 +20,7 @@ import android.support.v7.widget.Toolbar; | |||||||
| import android.util.Log; | import android.util.Log; | ||||||
| import android.util.SparseArray; | import android.util.SparseArray; | ||||||
| import android.view.LayoutInflater; | import android.view.LayoutInflater; | ||||||
|  | import android.view.MenuItem; | ||||||
| import android.view.View; | import android.view.View; | ||||||
| import android.view.ViewGroup; | import android.view.ViewGroup; | ||||||
| import android.widget.AdapterView; | import android.widget.AdapterView; | ||||||
| @@ -35,7 +42,8 @@ import org.schabi.newpipe.extractor.stream.StreamInfo; | |||||||
| import org.schabi.newpipe.extractor.stream.SubtitlesStream; | import org.schabi.newpipe.extractor.stream.SubtitlesStream; | ||||||
| import org.schabi.newpipe.extractor.stream.VideoStream; | import org.schabi.newpipe.extractor.stream.VideoStream; | ||||||
| import org.schabi.newpipe.extractor.utils.Localization; | import org.schabi.newpipe.extractor.utils.Localization; | ||||||
| import org.schabi.newpipe.settings.NewPipeSettings; | import org.schabi.newpipe.report.ErrorActivity; | ||||||
|  | import org.schabi.newpipe.report.UserAction; | ||||||
| import org.schabi.newpipe.util.FilenameUtils; | import org.schabi.newpipe.util.FilenameUtils; | ||||||
| import org.schabi.newpipe.util.ListHelper; | import org.schabi.newpipe.util.ListHelper; | ||||||
| import org.schabi.newpipe.util.PermissionHelper; | import org.schabi.newpipe.util.PermissionHelper; | ||||||
| @@ -44,20 +52,27 @@ import org.schabi.newpipe.util.StreamItemAdapter; | |||||||
| import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; | import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; | ||||||
| import org.schabi.newpipe.util.ThemeHelper; | import org.schabi.newpipe.util.ThemeHelper; | ||||||
|  |  | ||||||
|  | import java.io.IOException; | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
|  | import java.util.Collections; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Locale; | import java.util.Locale; | ||||||
|  |  | ||||||
| import icepick.Icepick; | import icepick.Icepick; | ||||||
| import icepick.State; | import icepick.State; | ||||||
| import io.reactivex.disposables.CompositeDisposable; | import io.reactivex.disposables.CompositeDisposable; | ||||||
|  | import us.shandian.giga.io.StoredDirectoryHelper; | ||||||
|  | import us.shandian.giga.io.StoredFileHelper; | ||||||
| import us.shandian.giga.postprocessing.Postprocessing; | import us.shandian.giga.postprocessing.Postprocessing; | ||||||
|  | import us.shandian.giga.service.DownloadManager; | ||||||
| import us.shandian.giga.service.DownloadManagerService; | import us.shandian.giga.service.DownloadManagerService; | ||||||
| import us.shandian.giga.service.DownloadManagerService.MissionCheck; | import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder; | ||||||
|  | import us.shandian.giga.service.MissionState; | ||||||
|  |  | ||||||
| public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener { | public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener { | ||||||
|     private static final String TAG = "DialogFragment"; |     private static final String TAG = "DialogFragment"; | ||||||
|     private static final boolean DEBUG = MainActivity.DEBUG; |     private static final boolean DEBUG = MainActivity.DEBUG; | ||||||
|  |     private static final int REQUEST_DOWNLOAD_PATH_SAF = 0x1230; | ||||||
|  |  | ||||||
|     @State |     @State | ||||||
|     protected StreamInfo currentInfo; |     protected StreamInfo currentInfo; | ||||||
| @@ -82,7 +97,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | |||||||
|  |  | ||||||
|     private EditText nameEditText; |     private EditText nameEditText; | ||||||
|     private Spinner streamsSpinner; |     private Spinner streamsSpinner; | ||||||
|     private RadioGroup radioVideoAudioGroup; |     private RadioGroup radioStreamsGroup; | ||||||
|     private TextView threadsCountTextView; |     private TextView threadsCountTextView; | ||||||
|     private SeekBar threadsSeekBar; |     private SeekBar threadsSeekBar; | ||||||
|  |  | ||||||
| @@ -162,7 +177,11 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | |||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(getContext())); |         final Context context = getContext(); | ||||||
|  |         if (context == null) | ||||||
|  |             throw new RuntimeException("Context was null"); | ||||||
|  |  | ||||||
|  |         setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context)); | ||||||
|         Icepick.restoreInstanceState(this, savedInstanceState); |         Icepick.restoreInstanceState(this, savedInstanceState); | ||||||
|  |  | ||||||
|         SparseArray<SecondaryStreamHelper<AudioStream>> secondaryStreams = new SparseArray<>(4); |         SparseArray<SecondaryStreamHelper<AudioStream>> secondaryStreams = new SparseArray<>(4); | ||||||
| @@ -179,9 +198,32 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         this.videoStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedVideoStreams, secondaryStreams); |         this.videoStreamsAdapter = new StreamItemAdapter<>(context, wrappedVideoStreams, secondaryStreams); | ||||||
|         this.audioStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedAudioStreams); |         this.audioStreamsAdapter = new StreamItemAdapter<>(context, wrappedAudioStreams); | ||||||
|         this.subtitleStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedSubtitleStreams); |         this.subtitleStreamsAdapter = new StreamItemAdapter<>(context, wrappedSubtitleStreams); | ||||||
|  |  | ||||||
|  |         Intent intent = new Intent(context, DownloadManagerService.class); | ||||||
|  |         context.startService(intent); | ||||||
|  |  | ||||||
|  |         context.bindService(intent, new ServiceConnection() { | ||||||
|  |             @Override | ||||||
|  |             public void onServiceConnected(ComponentName cname, IBinder service) { | ||||||
|  |                 DownloadManagerBinder mgr = (DownloadManagerBinder) service; | ||||||
|  |  | ||||||
|  |                 mainStorageAudio = mgr.getMainStorageAudio(); | ||||||
|  |                 mainStorageVideo = mgr.getMainStorageVideo(); | ||||||
|  |                 downloadManager = mgr.getDownloadManager(); | ||||||
|  |  | ||||||
|  |                 okButton.setEnabled(true); | ||||||
|  |  | ||||||
|  |                 context.unbindService(this); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @Override | ||||||
|  |             public void onServiceDisconnected(ComponentName name) { | ||||||
|  |                 // nothing to do | ||||||
|  |             } | ||||||
|  |         }, Context.BIND_AUTO_CREATE); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -206,8 +248,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | |||||||
|         threadsCountTextView = view.findViewById(R.id.threads_count); |         threadsCountTextView = view.findViewById(R.id.threads_count); | ||||||
|         threadsSeekBar = view.findViewById(R.id.threads); |         threadsSeekBar = view.findViewById(R.id.threads); | ||||||
|  |  | ||||||
|         radioVideoAudioGroup = view.findViewById(R.id.video_audio_group); |         radioStreamsGroup = view.findViewById(R.id.video_audio_group); | ||||||
|         radioVideoAudioGroup.setOnCheckedChangeListener(this); |         radioStreamsGroup.setOnCheckedChangeListener(this); | ||||||
|  |  | ||||||
|         initToolbar(view.findViewById(R.id.toolbar)); |         initToolbar(view.findViewById(R.id.toolbar)); | ||||||
|         setupDownloadOptions(); |         setupDownloadOptions(); | ||||||
| @@ -242,17 +284,17 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | |||||||
|         disposables.clear(); |         disposables.clear(); | ||||||
|  |  | ||||||
|         disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams).subscribe(result -> { |         disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams).subscribe(result -> { | ||||||
|             if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.video_button) { |             if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.video_button) { | ||||||
|                 setupVideoSpinner(); |                 setupVideoSpinner(); | ||||||
|             } |             } | ||||||
|         })); |         })); | ||||||
|         disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams).subscribe(result -> { |         disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams).subscribe(result -> { | ||||||
|             if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) { |             if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) { | ||||||
|                 setupAudioSpinner(); |                 setupAudioSpinner(); | ||||||
|             } |             } | ||||||
|         })); |         })); | ||||||
|         disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams).subscribe(result -> { |         disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams).subscribe(result -> { | ||||||
|             if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) { |             if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.subtitle_button) { | ||||||
|                 setupSubtitleSpinner(); |                 setupSubtitleSpinner(); | ||||||
|             } |             } | ||||||
|         })); |         })); | ||||||
| @@ -270,17 +312,40 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | |||||||
|         Icepick.saveInstanceState(this, outState); |         Icepick.saveInstanceState(this, outState); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onActivityResult(int requestCode, int resultCode, Intent data) { | ||||||
|  |         super.onActivityResult(requestCode, resultCode, data); | ||||||
|  |  | ||||||
|  |         if (requestCode == REQUEST_DOWNLOAD_PATH_SAF && resultCode == Activity.RESULT_OK) { | ||||||
|  |             if (data.getData() == null) { | ||||||
|  |                 showFailedDialog(R.string.general_error); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             try { | ||||||
|  |                 continueSelectedDownload(new StoredFileHelper(getContext(), data.getData(), "")); | ||||||
|  |             } catch (IOException e) { | ||||||
|  |                 showErrorActivity(e); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Inits |     // Inits | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|     private void initToolbar(Toolbar toolbar) { |     private void initToolbar(Toolbar toolbar) { | ||||||
|         if (DEBUG) Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]"); |         if (DEBUG) Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]"); | ||||||
|  |  | ||||||
|  |         boolean isLight = ThemeHelper.isLightThemeSelected(getActivity()); | ||||||
|  |         okButton = toolbar.findViewById(R.id.okay); | ||||||
|  |         okButton.setEnabled(false);// disable until the download service connection is done | ||||||
|  |  | ||||||
|         toolbar.setTitle(R.string.download_dialog_title); |         toolbar.setTitle(R.string.download_dialog_title); | ||||||
|         toolbar.setNavigationIcon(ThemeHelper.isLightThemeSelected(getActivity()) ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp); |         toolbar.setNavigationIcon(isLight ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp); | ||||||
|         toolbar.inflateMenu(R.menu.dialog_url); |         toolbar.inflateMenu(R.menu.dialog_url); | ||||||
|         toolbar.setNavigationOnClickListener(v -> getDialog().dismiss()); |         toolbar.setNavigationOnClickListener(v -> getDialog().dismiss()); | ||||||
|  |  | ||||||
|  |  | ||||||
|         toolbar.setOnMenuItemClickListener(item -> { |         toolbar.setOnMenuItemClickListener(item -> { | ||||||
|             if (item.getItemId() == R.id.okay) { |             if (item.getItemId() == R.id.okay) { | ||||||
|                 prepareSelectedDownload(); |                 prepareSelectedDownload(); | ||||||
| @@ -348,7 +413,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | |||||||
|     public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { |     public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { | ||||||
|         if (DEBUG) |         if (DEBUG) | ||||||
|             Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]"); |             Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]"); | ||||||
|         switch (radioVideoAudioGroup.getCheckedRadioButtonId()) { |         switch (radioStreamsGroup.getCheckedRadioButtonId()) { | ||||||
|             case R.id.audio_button: |             case R.id.audio_button: | ||||||
|                 selectedAudioIndex = position; |                 selectedAudioIndex = position; | ||||||
|                 break; |                 break; | ||||||
| @@ -372,9 +437,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | |||||||
|     protected void setupDownloadOptions() { |     protected void setupDownloadOptions() { | ||||||
|         setRadioButtonsState(false); |         setRadioButtonsState(false); | ||||||
|  |  | ||||||
|         final RadioButton audioButton = radioVideoAudioGroup.findViewById(R.id.audio_button); |         final RadioButton audioButton = radioStreamsGroup.findViewById(R.id.audio_button); | ||||||
|         final RadioButton videoButton = radioVideoAudioGroup.findViewById(R.id.video_button); |         final RadioButton videoButton = radioStreamsGroup.findViewById(R.id.video_button); | ||||||
|         final RadioButton subtitleButton = radioVideoAudioGroup.findViewById(R.id.subtitle_button); |         final RadioButton subtitleButton = radioStreamsGroup.findViewById(R.id.subtitle_button); | ||||||
|         final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0; |         final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0; | ||||||
|         final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0; |         final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0; | ||||||
|         final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0; |         final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0; | ||||||
| @@ -399,9 +464,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void setRadioButtonsState(boolean enabled) { |     private void setRadioButtonsState(boolean enabled) { | ||||||
|         radioVideoAudioGroup.findViewById(R.id.audio_button).setEnabled(enabled); |         radioStreamsGroup.findViewById(R.id.audio_button).setEnabled(enabled); | ||||||
|         radioVideoAudioGroup.findViewById(R.id.video_button).setEnabled(enabled); |         radioStreamsGroup.findViewById(R.id.video_button).setEnabled(enabled); | ||||||
|         radioVideoAudioGroup.findViewById(R.id.subtitle_button).setEnabled(enabled); |         radioStreamsGroup.findViewById(R.id.subtitle_button).setEnabled(enabled); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private int getSubtitleIndexBy(List<SubtitlesStream> streams) { |     private int getSubtitleIndexBy(List<SubtitlesStream> streams) { | ||||||
| @@ -436,56 +501,113 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | |||||||
|         return 0; |         return 0; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     StoredDirectoryHelper mainStorageAudio = null; | ||||||
|  |     StoredDirectoryHelper mainStorageVideo = null; | ||||||
|  |     DownloadManager downloadManager = null; | ||||||
|  |  | ||||||
|  |     MenuItem okButton = null; | ||||||
|  |  | ||||||
|  |     private String getNameEditText() { | ||||||
|  |         return nameEditText.getText().toString().trim(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void showFailedDialog(@StringRes int msg) { | ||||||
|  |         new AlertDialog.Builder(getContext()) | ||||||
|  |                 .setMessage(msg) | ||||||
|  |                 .setNegativeButton(android.R.string.ok, null) | ||||||
|  |                 .create() | ||||||
|  |                 .show(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void showErrorActivity(Exception e) { | ||||||
|  |         ErrorActivity.reportError( | ||||||
|  |                 getContext(), | ||||||
|  |                 Collections.singletonList(e), | ||||||
|  |                 null, | ||||||
|  |                 null, | ||||||
|  |                 ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "-", "-", R.string.general_error) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     private void prepareSelectedDownload() { |     private void prepareSelectedDownload() { | ||||||
|         final Context context = getContext(); |         final Context context = getContext(); | ||||||
|         Stream stream; |         StoredDirectoryHelper mainStorage; | ||||||
|         String location; |         MediaFormat format; | ||||||
|         char kind; |         String mime; | ||||||
|  |  | ||||||
|         String fileName = nameEditText.getText().toString().trim(); |         // first, build the filename and get the output folder (if possible) | ||||||
|         if (fileName.isEmpty()) |  | ||||||
|             fileName = FilenameUtils.createFilename(context, currentInfo.getName()); |  | ||||||
|  |  | ||||||
|         switch (radioVideoAudioGroup.getCheckedRadioButtonId()) { |         String filename = getNameEditText() + "."; | ||||||
|  |         if (filename.isEmpty()) { | ||||||
|  |             filename = FilenameUtils.createFilename(context, currentInfo.getName()); | ||||||
|  |         } | ||||||
|  |         filename += "."; | ||||||
|  |  | ||||||
|  |         switch (radioStreamsGroup.getCheckedRadioButtonId()) { | ||||||
|             case R.id.audio_button: |             case R.id.audio_button: | ||||||
|                 stream = audioStreamsAdapter.getItem(selectedAudioIndex); |                 mainStorage = mainStorageAudio; | ||||||
|                 location = NewPipeSettings.getAudioDownloadPath(context); |                 format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat(); | ||||||
|                 kind = 'a'; |                 mime = format.mimeType; | ||||||
|  |                 filename += format.suffix; | ||||||
|                 break; |                 break; | ||||||
|             case R.id.video_button: |             case R.id.video_button: | ||||||
|                 stream = videoStreamsAdapter.getItem(selectedVideoIndex); |                 mainStorage = mainStorageVideo; | ||||||
|                 location = NewPipeSettings.getVideoDownloadPath(context); |                 format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat(); | ||||||
|                 kind = 'v'; |                 mime = format.mimeType; | ||||||
|  |                 filename += format.suffix; | ||||||
|                 break; |                 break; | ||||||
|             case R.id.subtitle_button: |             case R.id.subtitle_button: | ||||||
|                 stream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex); |                 mainStorage = mainStorageVideo;// subtitle & video files go together | ||||||
|                 location = NewPipeSettings.getVideoDownloadPath(context);// assume that subtitle & video files go together |                 format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat(); | ||||||
|                 kind = 's'; |                 mime = format.mimeType; | ||||||
|  |                 filename += format == MediaFormat.TTML ? MediaFormat.SRT.suffix : format.suffix; | ||||||
|                 break; |                 break; | ||||||
|             default: |             default: | ||||||
|  |                 throw new RuntimeException("No stream selected"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (mainStorage == null) { | ||||||
|  |             // this part is called if... | ||||||
|  |             //                          older android version running with SAF preferred | ||||||
|  |             //                          save path not defined (via download settings) | ||||||
|  |  | ||||||
|  |             StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_PATH_SAF, filename, mime); | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         int threads; |         // check for existing file with the same name | ||||||
|  |         Uri result = mainStorage.findFile(filename); | ||||||
|  |  | ||||||
|         if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) { |         if (result == null) { | ||||||
|             threads = 1;// use unique thread for subtitles due small file size |             // the file does not exists, create | ||||||
|             fileName += ".srt";// final subtitle format |             StoredFileHelper storage = mainStorage.createFile(filename, mime); | ||||||
|         } else { |             if (storage == null || !storage.canWrite()) { | ||||||
|             threads = threadsSeekBar.getProgress() + 1; |                 showFailedDialog(R.string.error_file_creation); | ||||||
|             fileName += "." + stream.getFormat().getSuffix(); |                 return; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|         final String finalFileName = fileName; |             continueSelectedDownload(storage); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         DownloadManagerService.checkForRunningMission(context, location, fileName, (MissionCheck result) -> { |         // the target filename is already use, try load | ||||||
|  |         StoredFileHelper storage; | ||||||
|  |         try { | ||||||
|  |             storage = new StoredFileHelper(context, result, mime); | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             showErrorActivity(e); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // check if is our file | ||||||
|  |         MissionState state = downloadManager.checkForExistingMission(storage); | ||||||
|         @StringRes int msgBtn; |         @StringRes int msgBtn; | ||||||
|         @StringRes int msgBody; |         @StringRes int msgBody; | ||||||
|  |  | ||||||
|             switch (result) { |         switch (state) { | ||||||
|             case Finished: |             case Finished: | ||||||
|                 msgBtn = R.string.overwrite; |                 msgBtn = R.string.overwrite; | ||||||
|                     msgBody = R.string.overwrite_warning; |                 msgBody = R.string.overwrite_finished_warning; | ||||||
|                 break; |                 break; | ||||||
|             case Pending: |             case Pending: | ||||||
|                 msgBtn = R.string.overwrite; |                 msgBtn = R.string.overwrite; | ||||||
| @@ -495,45 +617,108 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | |||||||
|                 msgBtn = R.string.generate_unique_name; |                 msgBtn = R.string.generate_unique_name; | ||||||
|                 msgBody = R.string.download_already_running; |                 msgBody = R.string.download_already_running; | ||||||
|                 break; |                 break; | ||||||
|  |             case None: | ||||||
|  |                 msgBtn = R.string.overwrite; | ||||||
|  |                 msgBody = R.string.overwrite_unrelated_warning; | ||||||
|  |                 break; | ||||||
|             default: |             default: | ||||||
|                     downloadSelected(context, stream, location, finalFileName, kind, threads); |  | ||||||
|                 return; |                 return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|             // overwrite or unique name actions are done by the download manager |         // handle user answer (overwrite or create another file with different name) | ||||||
|  |         final String finalFilename = filename; | ||||||
|         AlertDialog.Builder builder = new AlertDialog.Builder(context); |         AlertDialog.Builder builder = new AlertDialog.Builder(context); | ||||||
|         builder.setTitle(R.string.download_dialog_title) |         builder.setTitle(R.string.download_dialog_title) | ||||||
|                 .setMessage(msgBody) |                 .setMessage(msgBody) | ||||||
|                     .setPositiveButton( |                 .setPositiveButton(msgBtn, (dialog, which) -> { | ||||||
|                             msgBtn, |                     dialog.dismiss(); | ||||||
|                             (dialog, which) -> downloadSelected(context, stream, location, finalFileName, kind, threads) |  | ||||||
|                     ) |                     StoredFileHelper storageNew; | ||||||
|                     .setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel()) |                     switch (state) { | ||||||
|  |                         case Finished: | ||||||
|  |                         case Pending: | ||||||
|  |                             downloadManager.forgetMission(storage); | ||||||
|  |                         case None: | ||||||
|  |                             // try take (or steal) the file permissions | ||||||
|  |                             try { | ||||||
|  |                                 storageNew = new StoredFileHelper(context, result, mainStorage.getTag()); | ||||||
|  |                                 if (storageNew.canWrite()) | ||||||
|  |                                     continueSelectedDownload(storageNew); | ||||||
|  |                                 else | ||||||
|  |                                     showFailedDialog(R.string.error_file_creation); | ||||||
|  |                             } catch (IOException e) { | ||||||
|  |                                 showErrorActivity(e); | ||||||
|  |                             } | ||||||
|  |                             break; | ||||||
|  |                         case PendingRunning: | ||||||
|  |                             // FIXME: createUniqueFile() is not tested properly | ||||||
|  |                             storageNew = mainStorage.createUniqueFile(finalFilename, mime); | ||||||
|  |                             if (storageNew == null) | ||||||
|  |                                 showFailedDialog(R.string.error_file_creation); | ||||||
|  |                             else | ||||||
|  |                                 continueSelectedDownload(storageNew); | ||||||
|  |                             break; | ||||||
|  |                     } | ||||||
|  |                 }) | ||||||
|  |                 .setNegativeButton(android.R.string.cancel, null) | ||||||
|                 .create() |                 .create() | ||||||
|                 .show(); |                 .show(); | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void downloadSelected(Context context, Stream selectedStream, String location, String fileName, char kind, int threads) { |     private void continueSelectedDownload(@NonNull StoredFileHelper storage) { | ||||||
|  |         final Context context = getContext(); | ||||||
|  |  | ||||||
|  |         if (!storage.canWrite()) { | ||||||
|  |             showFailedDialog(R.string.permission_denied); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // check if the selected file has to be overwritten, by simply checking its length | ||||||
|  |         try { | ||||||
|  |             if (storage.length() > 0) storage.truncate(); | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             Log.e(TAG, "failed to overwrite the file: " + storage.getUri().toString(), e); | ||||||
|  |             //showErrorActivity(e); | ||||||
|  |             showFailedDialog(R.string.overwrite_failed); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Stream selectedStream; | ||||||
|  |         char kind; | ||||||
|  |         int threads = threadsSeekBar.getProgress() + 1; | ||||||
|         String[] urls; |         String[] urls; | ||||||
|         String psName = null; |         String psName = null; | ||||||
|         String[] psArgs = null; |         String[] psArgs = null; | ||||||
|         String secondaryStreamUrl = null; |         String secondaryStreamUrl = null; | ||||||
|         long nearLength = 0; |         long nearLength = 0; | ||||||
|  |  | ||||||
|         if (selectedStream instanceof AudioStream) { |         // more download logic: select muxer, subtitle converter, etc. | ||||||
|  |         switch (radioStreamsGroup.getCheckedRadioButtonId()) { | ||||||
|  |             case R.id.audio_button: | ||||||
|  |                 threads = 1;// use unique thread for subtitles due small file size | ||||||
|  |                 kind = 'a'; | ||||||
|  |                 selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex); | ||||||
|  |  | ||||||
|                 if (selectedStream.getFormat() == MediaFormat.M4A) { |                 if (selectedStream.getFormat() == MediaFormat.M4A) { | ||||||
|                     psName = Postprocessing.ALGORITHM_M4A_NO_DASH; |                     psName = Postprocessing.ALGORITHM_M4A_NO_DASH; | ||||||
|                 } |                 } | ||||||
|         } else if (selectedStream instanceof VideoStream) { |                 break; | ||||||
|  |             case R.id.video_button: | ||||||
|  |                 kind = 'v'; | ||||||
|  |                 selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex); | ||||||
|  |  | ||||||
|                 SecondaryStreamHelper<AudioStream> secondaryStream = videoStreamsAdapter |                 SecondaryStreamHelper<AudioStream> secondaryStream = videoStreamsAdapter | ||||||
|                         .getAllSecondary() |                         .getAllSecondary() | ||||||
|                         .get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream)); |                         .get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream)); | ||||||
|  |  | ||||||
|                 if (secondaryStream != null) { |                 if (secondaryStream != null) { | ||||||
|                     secondaryStreamUrl = secondaryStream.getStream().getUrl(); |                     secondaryStreamUrl = secondaryStream.getStream().getUrl(); | ||||||
|                 psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER; |  | ||||||
|  |                     if (selectedStream.getFormat() == MediaFormat.MPEG_4) | ||||||
|  |                         psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER; | ||||||
|  |                     else | ||||||
|  |                         psName = Postprocessing.ALGORITHM_WEBM_MUXER; | ||||||
|  |  | ||||||
|                     psArgs = null; |                     psArgs = null; | ||||||
|                     long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream); |                     long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream); | ||||||
|  |  | ||||||
| @@ -542,7 +727,12 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | |||||||
|                         nearLength = secondaryStream.getSizeInBytes() + videoSize; |                         nearLength = secondaryStream.getSizeInBytes() + videoSize; | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|         } else if ((selectedStream instanceof SubtitlesStream) && selectedStream.getFormat() == MediaFormat.TTML) { |                 break; | ||||||
|  |             case R.id.subtitle_button: | ||||||
|  |                 kind = 's'; | ||||||
|  |                 selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex); | ||||||
|  |  | ||||||
|  |                 if (selectedStream.getFormat() == MediaFormat.TTML) { | ||||||
|                     psName = Postprocessing.ALGORITHM_TTML_CONVERTER; |                     psName = Postprocessing.ALGORITHM_TTML_CONVERTER; | ||||||
|                     psArgs = new String[]{ |                     psArgs = new String[]{ | ||||||
|                             selectedStream.getFormat().getSuffix(), |                             selectedStream.getFormat().getSuffix(), | ||||||
| @@ -550,6 +740,10 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | |||||||
|                             "false",// detect youtube duplicate lines |                             "false",// detect youtube duplicate lines | ||||||
|                     }; |                     }; | ||||||
|                 } |                 } | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         if (secondaryStreamUrl == null) { |         if (secondaryStreamUrl == null) { | ||||||
|             urls = new String[]{selectedStream.getUrl()}; |             urls = new String[]{selectedStream.getUrl()}; | ||||||
| @@ -557,8 +751,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck | |||||||
|             urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl}; |             urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl}; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         DownloadManagerService.startMission(context, urls, location, fileName, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength); |         DownloadManagerService.startMission(context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength); | ||||||
|  |  | ||||||
|         getDialog().dismiss(); |         dismiss(); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -21,8 +21,8 @@ import org.schabi.newpipe.extractor.NewPipe; | |||||||
| import org.schabi.newpipe.extractor.channel.ChannelInfo; | import org.schabi.newpipe.extractor.channel.ChannelInfo; | ||||||
| import org.schabi.newpipe.extractor.exceptions.ExtractionException; | import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||||
| import org.schabi.newpipe.fragments.list.BaseListFragment; | import org.schabi.newpipe.fragments.list.BaseListFragment; | ||||||
| import org.schabi.newpipe.report.UserAction; |  | ||||||
| import org.schabi.newpipe.local.subscription.SubscriptionService; | import org.schabi.newpipe.local.subscription.SubscriptionService; | ||||||
|  | import org.schabi.newpipe.report.UserAction; | ||||||
|  |  | ||||||
| import java.util.Collections; | import java.util.Collections; | ||||||
| import java.util.HashSet; | import java.util.HashSet; | ||||||
| @@ -262,7 +262,7 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi | |||||||
|      * If chosen feed already displayed, then we request another feed from another |      * If chosen feed already displayed, then we request another feed from another | ||||||
|      * subscription, until the subscription table runs out of new items. |      * subscription, until the subscription table runs out of new items. | ||||||
|      * <p> |      * <p> | ||||||
|      * This Observer is self-contained and will dispose itself when complete. However, this |      * This Observer is self-contained and will close itself when complete. However, this | ||||||
|      * does not obey the fragment lifecycle and may continue running in the background |      * does not obey the fragment lifecycle and may continue running in the background | ||||||
|      * until it is complete. This is done due to RxJava2 no longer propagate errors once |      * until it is complete. This is done due to RxJava2 no longer propagate errors once | ||||||
|      * an observer is unsubscribed while the thread process is still running. |      * an observer is unsubscribed while the thread process is still running. | ||||||
|   | |||||||
| @@ -158,7 +158,7 @@ public class MediaSourceManager { | |||||||
|      * Dispose the manager and releases all message buses and loaders. |      * Dispose the manager and releases all message buses and loaders. | ||||||
|      * */ |      * */ | ||||||
|     public void dispose() { |     public void dispose() { | ||||||
|         if (DEBUG) Log.d(TAG, "dispose() called."); |         if (DEBUG) Log.d(TAG, "close() called."); | ||||||
|  |  | ||||||
|         debouncedSignal.onComplete(); |         debouncedSignal.onComplete(); | ||||||
|         debouncedLoader.dispose(); |         debouncedLoader.dispose(); | ||||||
|   | |||||||
| @@ -2,26 +2,42 @@ package org.schabi.newpipe.settings; | |||||||
|  |  | ||||||
| import android.app.Activity; | import android.app.Activity; | ||||||
| import android.app.AlertDialog; | import android.app.AlertDialog; | ||||||
|  | import android.content.Context; | ||||||
| import android.content.Intent; | import android.content.Intent; | ||||||
|  | import android.net.Uri; | ||||||
|  | import android.os.Build; | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
| import android.support.annotation.Nullable; | import android.support.annotation.Nullable; | ||||||
|  | import android.support.annotation.RequiresApi; | ||||||
|  | import android.support.annotation.StringRes; | ||||||
| import android.support.v7.preference.Preference; | import android.support.v7.preference.Preference; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
|  | import android.widget.Toast; | ||||||
| import com.nononsenseapps.filepicker.Utils; |  | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.util.FilePickerActivityHelper; | import org.schabi.newpipe.util.FilePickerActivityHelper; | ||||||
|  |  | ||||||
| import java.io.File; | import java.io.File; | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.net.URI; | ||||||
|  |  | ||||||
|  | import us.shandian.giga.io.StoredDirectoryHelper; | ||||||
|  |  | ||||||
| public class DownloadSettingsFragment extends BasePreferenceFragment { | public class DownloadSettingsFragment extends BasePreferenceFragment { | ||||||
|     private static final int REQUEST_DOWNLOAD_PATH = 0x1235; |     private static final int REQUEST_DOWNLOAD_VIDEO_PATH = 0x1235; | ||||||
|     private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236; |     private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236; | ||||||
|  |  | ||||||
|     private String DOWNLOAD_PATH_PREFERENCE; |     private String DOWNLOAD_PATH_VIDEO_PREFERENCE; | ||||||
|     private String DOWNLOAD_PATH_AUDIO_PREFERENCE; |     private String DOWNLOAD_PATH_AUDIO_PREFERENCE; | ||||||
|  |  | ||||||
|  |     private String DOWNLOAD_STORAGE_API; | ||||||
|  |     private String DOWNLOAD_STORAGE_API_DEFAULT; | ||||||
|  |  | ||||||
|  |     private Preference prefPathVideo; | ||||||
|  |     private Preference prefPathAudio; | ||||||
|  |      | ||||||
|  |     private Context ctx; | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void onCreate(@Nullable Bundle savedInstanceState) { |     public void onCreate(@Nullable Bundle savedInstanceState) { | ||||||
|         super.onCreate(savedInstanceState); |         super.onCreate(savedInstanceState); | ||||||
| @@ -33,16 +49,100 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { | |||||||
|     @Override |     @Override | ||||||
|     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { |     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { | ||||||
|         addPreferencesFromResource(R.xml.download_settings); |         addPreferencesFromResource(R.xml.download_settings); | ||||||
|  |  | ||||||
|  |         prefPathVideo = findPreference(DOWNLOAD_PATH_VIDEO_PREFERENCE); | ||||||
|  |         prefPathAudio = findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE); | ||||||
|  |  | ||||||
|  |         updatePathPickers(usingJavaIO()); | ||||||
|  |  | ||||||
|  |         findPreference(DOWNLOAD_STORAGE_API).setOnPreferenceChangeListener((preference, value) -> { | ||||||
|  |             boolean javaIO = DOWNLOAD_STORAGE_API_DEFAULT.equals(value); | ||||||
|  |  | ||||||
|  |             if (!javaIO && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||||||
|  |                 Toast.makeText(ctx, R.string.download_pick_path, Toast.LENGTH_LONG).show(); | ||||||
|  |  | ||||||
|  |                 // forget save paths | ||||||
|  |                 forgetSAFTree(DOWNLOAD_PATH_VIDEO_PREFERENCE); | ||||||
|  |                 forgetSAFTree(DOWNLOAD_PATH_AUDIO_PREFERENCE); | ||||||
|  |  | ||||||
|  |                 defaultPreferences.edit() | ||||||
|  |                         .putString(DOWNLOAD_PATH_VIDEO_PREFERENCE, "") | ||||||
|  |                         .putString(DOWNLOAD_PATH_AUDIO_PREFERENCE, "") | ||||||
|  |                         .apply(); | ||||||
|  |  | ||||||
|  |                 updatePreferencesSummary(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             updatePathPickers(javaIO); | ||||||
|  |             return true; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onAttach(Context context) { | ||||||
|  |         super.onAttach(context); | ||||||
|  |         ctx = context; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onDetach() { | ||||||
|  |         super.onDetach(); | ||||||
|  |         ctx = null; | ||||||
|  |         findPreference(DOWNLOAD_STORAGE_API).setOnPreferenceChangeListener(null); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void initKeys() { |     private void initKeys() { | ||||||
|         DOWNLOAD_PATH_PREFERENCE = getString(R.string.download_path_key); |         DOWNLOAD_PATH_VIDEO_PREFERENCE = getString(R.string.download_path_video_key); | ||||||
|         DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key); |         DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key); | ||||||
|  |         DOWNLOAD_STORAGE_API = getString(R.string.downloads_storage_api); | ||||||
|  |         DOWNLOAD_STORAGE_API_DEFAULT = getString(R.string.downloads_storage_api_default); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void updatePreferencesSummary() { |     private void updatePreferencesSummary() { | ||||||
|         findPreference(DOWNLOAD_PATH_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_PREFERENCE, getString(R.string.download_path_summary))); |         prefPathVideo.setSummary( | ||||||
|         findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, getString(R.string.download_path_audio_summary))); |                 defaultPreferences.getString(DOWNLOAD_PATH_VIDEO_PREFERENCE, getString(R.string.download_path_summary)) | ||||||
|  |         ); | ||||||
|  |         prefPathAudio.setSummary( | ||||||
|  |                 defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, getString(R.string.download_path_audio_summary)) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void updatePathPickers(boolean useJavaIO) { | ||||||
|  |         boolean enabled = useJavaIO || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; | ||||||
|  |         prefPathVideo.setEnabled(enabled); | ||||||
|  |         prefPathAudio.setEnabled(enabled); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private boolean usingJavaIO() { | ||||||
|  |         return DOWNLOAD_STORAGE_API_DEFAULT.equals( | ||||||
|  |                 defaultPreferences.getString(DOWNLOAD_STORAGE_API, DOWNLOAD_STORAGE_API_DEFAULT) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||||
|  |     private void forgetSAFTree(String prefKey) { | ||||||
|  |  | ||||||
|  |         String oldPath = defaultPreferences.getString(prefKey, ""); | ||||||
|  |  | ||||||
|  |         if (oldPath != null && !oldPath.isEmpty() && oldPath.charAt(0) != File.separatorChar) { | ||||||
|  |             try { | ||||||
|  |                 StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(ctx, Uri.parse(oldPath), null); | ||||||
|  |                 if (!mainStorage.isDirect()) { | ||||||
|  |                     mainStorage.revokePermissions(); | ||||||
|  |                     Log.i(TAG, "revokePermissions()  [uri=" + oldPath + "]  ¡success!"); | ||||||
|  |                 } | ||||||
|  |             } catch (IOException err) { | ||||||
|  |                 Log.e(TAG, "Error revoking Tree uri permissions  [uri=" + oldPath + "]", err); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void showMessageDialog(@StringRes int title, @StringRes int message) { | ||||||
|  |         AlertDialog.Builder msg = new AlertDialog.Builder(ctx); | ||||||
|  |         msg.setTitle(title); | ||||||
|  |         msg.setMessage(message); | ||||||
|  |         msg.setPositiveButton(android.R.string.ok, null); | ||||||
|  |         msg.show(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -51,17 +151,31 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { | |||||||
|             Log.d(TAG, "onPreferenceTreeClick() called with: preference = [" + preference + "]"); |             Log.d(TAG, "onPreferenceTreeClick() called with: preference = [" + preference + "]"); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE) |         String key = preference.getKey(); | ||||||
|                 || preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) { |  | ||||||
|             Intent i = new Intent(getActivity(), FilePickerActivityHelper.class) |         if (key.equals(DOWNLOAD_PATH_VIDEO_PREFERENCE) || key.equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) { | ||||||
|  |             boolean safPick = !usingJavaIO(); | ||||||
|  |  | ||||||
|  |             int request = 0; | ||||||
|  |             if (key.equals(DOWNLOAD_PATH_VIDEO_PREFERENCE)) { | ||||||
|  |                 request = REQUEST_DOWNLOAD_VIDEO_PATH; | ||||||
|  |             } else if (key.equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) { | ||||||
|  |                 request = REQUEST_DOWNLOAD_AUDIO_PATH; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             Intent i; | ||||||
|  |             if (safPick && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||||||
|  |                 i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) | ||||||
|  |                         .putExtra("android.content.extra.SHOW_ADVANCED", true) | ||||||
|  |                         .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS); | ||||||
|  |             } else { | ||||||
|  |                 i = new Intent(getActivity(), FilePickerActivityHelper.class) | ||||||
|                         .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) |                         .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) | ||||||
|                         .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) |                         .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) | ||||||
|                         .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_DIR); |                         .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_DIR); | ||||||
|             if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE)) { |  | ||||||
|                 startActivityForResult(i, REQUEST_DOWNLOAD_PATH); |  | ||||||
|             } else if (preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) { |  | ||||||
|                 startActivityForResult(i, REQUEST_DOWNLOAD_AUDIO_PATH); |  | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             startActivityForResult(i, request); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return super.onPreferenceTreeClick(preference); |         return super.onPreferenceTreeClick(preference); | ||||||
| @@ -71,25 +185,50 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { | |||||||
|     public void onActivityResult(int requestCode, int resultCode, Intent data) { |     public void onActivityResult(int requestCode, int resultCode, Intent data) { | ||||||
|         super.onActivityResult(requestCode, resultCode, data); |         super.onActivityResult(requestCode, resultCode, data); | ||||||
|         if (DEBUG) { |         if (DEBUG) { | ||||||
|             Log.d(TAG, "onActivityResult() called with: requestCode = [" + requestCode + "], resultCode = [" + resultCode + "], data = [" + data + "]"); |             Log.d(TAG, "onActivityResult() called with: requestCode = [" + requestCode + "], " + | ||||||
|  |                     "resultCode = [" + resultCode + "], data = [" + data + "]" | ||||||
|  |             ); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if ((requestCode == REQUEST_DOWNLOAD_PATH || requestCode == REQUEST_DOWNLOAD_AUDIO_PATH) |         if (resultCode != Activity.RESULT_OK) return; | ||||||
|                 && resultCode == Activity.RESULT_OK && data.getData() != null) { |  | ||||||
|             String key = getString(requestCode == REQUEST_DOWNLOAD_PATH ? R.string.download_path_key : R.string.download_path_audio_key); |  | ||||||
|             String path = Utils.getFileForUri(data.getData()).getAbsolutePath(); |  | ||||||
|  |  | ||||||
|             defaultPreferences.edit().putString(key, path).apply(); |         String key; | ||||||
|  |         if (requestCode == REQUEST_DOWNLOAD_VIDEO_PATH) | ||||||
|  |             key = DOWNLOAD_PATH_VIDEO_PREFERENCE; | ||||||
|  |         else if (requestCode == REQUEST_DOWNLOAD_AUDIO_PATH) | ||||||
|  |             key = DOWNLOAD_PATH_AUDIO_PREFERENCE; | ||||||
|  |         else | ||||||
|  |             return; | ||||||
|  |  | ||||||
|  |         Uri uri = data.getData(); | ||||||
|  |         if (uri == null) { | ||||||
|  |             showMessageDialog(R.string.general_error, R.string.invalid_directory); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!usingJavaIO() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||||||
|  |             // steps: | ||||||
|  |             //       1. acquire permissions on the new save path | ||||||
|  |             //       2. save the new path, if step(1) was successful | ||||||
|  |  | ||||||
|  |             try { | ||||||
|  |                 StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(ctx, uri, null); | ||||||
|  |                 mainStorage.acquirePermissions(); | ||||||
|  |                 Log.i(TAG, "acquirePermissions()  [uri=" + uri.toString() + "]  ¡success!"); | ||||||
|  |             } catch (IOException err) { | ||||||
|  |                 Log.e(TAG, "Error acquiring permissions on " + uri.toString()); | ||||||
|  |                 showMessageDialog(R.string.general_error, R.string.no_available_dir); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             defaultPreferences.edit().putString(key, uri.toString()).apply(); | ||||||
|  |         } else { | ||||||
|  |             defaultPreferences.edit().putString(key, uri.toString()).apply(); | ||||||
|             updatePreferencesSummary(); |             updatePreferencesSummary(); | ||||||
|  |  | ||||||
|             File target = new File(path); |             File target = new File(URI.create(uri.toString())); | ||||||
|             if (!target.canWrite()) { |             if (!target.canWrite()) | ||||||
|                 AlertDialog.Builder msg = new AlertDialog.Builder(getContext()); |                 showMessageDialog(R.string.download_to_sdcard_error_title, R.string.download_to_sdcard_error_message); | ||||||
|                 msg.setTitle(R.string.download_to_sdcard_error_title); |  | ||||||
|                 msg.setMessage(R.string.download_to_sdcard_error_message); |  | ||||||
|                 msg.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { }); |  | ||||||
|                 msg.show(); |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -70,37 +70,23 @@ public class NewPipeSettings { | |||||||
|         getAudioDownloadFolder(context); |         getAudioDownloadFolder(context); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static File getVideoDownloadFolder(Context context) { |     private static void getVideoDownloadFolder(Context context) { | ||||||
|         return getDir(context, R.string.download_path_key, Environment.DIRECTORY_MOVIES); |         getDir(context, R.string.download_path_video_key, Environment.DIRECTORY_MOVIES); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static String getVideoDownloadPath(Context context) { |     private static void getAudioDownloadFolder(Context context) { | ||||||
|         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); |         getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC); | ||||||
|         final String key = context.getString(R.string.download_path_key); |  | ||||||
|         return prefs.getString(key, Environment.DIRECTORY_MOVIES); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static File getAudioDownloadFolder(Context context) { |     private static void getDir(Context context, int keyID, String defaultDirectoryName) { | ||||||
|         return getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static String getAudioDownloadPath(Context context) { |  | ||||||
|         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); |  | ||||||
|         final String key = context.getString(R.string.download_path_audio_key); |  | ||||||
|         return prefs.getString(key, Environment.DIRECTORY_MUSIC); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private static File getDir(Context context, int keyID, String defaultDirectoryName) { |  | ||||||
|         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); |         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); | ||||||
|         final String key = context.getString(keyID); |         final String key = context.getString(keyID); | ||||||
|         String downloadPath = prefs.getString(key, null); |         String downloadPath = prefs.getString(key, null); | ||||||
|         if ((downloadPath != null) && (!downloadPath.isEmpty())) return new File(downloadPath.trim()); |         if ((downloadPath != null) && (!downloadPath.isEmpty())) return; | ||||||
|  |  | ||||||
|         final File dir = getDir(defaultDirectoryName); |  | ||||||
|         SharedPreferences.Editor spEditor = prefs.edit(); |         SharedPreferences.Editor spEditor = prefs.edit(); | ||||||
|         spEditor.putString(key, getNewPipeChildFolderPathForDir(dir)); |         spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName))); | ||||||
|         spEditor.apply(); |         spEditor.apply(); | ||||||
|         return dir; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @NonNull |     @NonNull | ||||||
| @@ -110,8 +96,13 @@ public class NewPipeSettings { | |||||||
|  |  | ||||||
|     public static void resetDownloadFolders(Context context) { |     public static void resetDownloadFolders(Context context) { | ||||||
|         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); |         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); | ||||||
|  |  | ||||||
|  |         prefs.edit() | ||||||
|  |                 .putString(context.getString(R.string.downloads_storage_api), context.getString(R.string.downloads_storage_api_default)) | ||||||
|  |                 .apply(); | ||||||
|  |  | ||||||
|         resetDownloadFolder(prefs, context.getString(R.string.download_path_audio_key), Environment.DIRECTORY_MUSIC); |         resetDownloadFolder(prefs, context.getString(R.string.download_path_audio_key), Environment.DIRECTORY_MUSIC); | ||||||
|         resetDownloadFolder(prefs, context.getString(R.string.download_path_key), Environment.DIRECTORY_MOVIES); |         resetDownloadFolder(prefs, context.getString(R.string.download_path_video_key), Environment.DIRECTORY_MOVIES); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private static void resetDownloadFolder(SharedPreferences prefs, String key, String defaultDirectoryName) { |     private static void resetDownloadFolder(SharedPreferences prefs, String key, String defaultDirectoryName) { | ||||||
|   | |||||||
| @@ -120,7 +120,7 @@ public class Mp4FromDashWriter { | |||||||
|         parsed = true; |         parsed = true; | ||||||
|  |  | ||||||
|         for (SharpStream src : sourceTracks) { |         for (SharpStream src : sourceTracks) { | ||||||
|             src.dispose(); |             src.close(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         tracks = null; |         tracks = null; | ||||||
|   | |||||||
| @@ -107,7 +107,7 @@ public class WebMWriter { | |||||||
|         parsed = true; |         parsed = true; | ||||||
|  |  | ||||||
|         for (SharpStream src : sourceTracks) { |         for (SharpStream src : sourceTracks) { | ||||||
|             src.dispose(); |             src.close(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         sourceTracks = null; |         sourceTracks = null; | ||||||
|   | |||||||
| @@ -1,11 +1,12 @@ | |||||||
| package org.schabi.newpipe.streams.io; | package org.schabi.newpipe.streams.io; | ||||||
|  |  | ||||||
|  | import java.io.Closeable; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * based on c# |  * based on c# | ||||||
|  */ |  */ | ||||||
| public abstract class SharpStream { | public abstract class SharpStream  implements Closeable { | ||||||
|  |  | ||||||
|     public abstract int read() throws IOException; |     public abstract int read() throws IOException; | ||||||
|  |  | ||||||
| @@ -19,9 +20,10 @@ public abstract class SharpStream { | |||||||
|  |  | ||||||
|     public abstract void rewind() throws IOException; |     public abstract void rewind() throws IOException; | ||||||
|  |  | ||||||
|     public abstract void dispose(); |     public abstract boolean isClosed(); | ||||||
|  |  | ||||||
|     public abstract boolean isDisposed(); |     @Override | ||||||
|  |     public abstract void close(); | ||||||
|  |  | ||||||
|     public abstract boolean canRewind(); |     public abstract boolean canRewind(); | ||||||
|  |  | ||||||
| @@ -54,4 +56,8 @@ public abstract class SharpStream { | |||||||
|     public void seek(long offset) throws IOException { |     public void seek(long offset) throws IOException { | ||||||
|         throw new IOException("Not implemented"); |         throw new IOException("Not implemented"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public long length() throws IOException { | ||||||
|  |         throw new UnsupportedOperationException("Unsupported operation"); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,10 +3,10 @@ package us.shandian.giga.get; | |||||||
| import android.support.annotation.NonNull; | import android.support.annotation.NonNull; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
|  |  | ||||||
| import java.io.File; | import org.schabi.newpipe.streams.io.SharpStream; | ||||||
|  |  | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.io.InterruptedIOException; | import java.io.InterruptedIOException; | ||||||
| import java.io.RandomAccessFile; |  | ||||||
| import java.net.HttpURLConnection; | import java.net.HttpURLConnection; | ||||||
| import java.nio.channels.ClosedByInterruptException; | import java.nio.channels.ClosedByInterruptException; | ||||||
|  |  | ||||||
| @@ -111,34 +111,10 @@ public class DownloadInitializer extends Thread { | |||||||
|                     if (!mMission.running || Thread.interrupted()) return; |                     if (!mMission.running || Thread.interrupted()) return; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 File file; |                 SharpStream fs = mMission.storage.getStream(); | ||||||
|                 if (mMission.current == 0) { |                 fs.setLength(mMission.offsets[mMission.current] + mMission.length); | ||||||
|                     file = new File(mMission.location); |                 fs.seek(mMission.offsets[mMission.current]); | ||||||
|                     if (!Utility.mkdir(file, true)) { |                 fs.close(); | ||||||
|                         mMission.notifyError(DownloadMission.ERROR_PATH_CREATION, null); |  | ||||||
|                         return; |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     file = new File(file, mMission.name); |  | ||||||
|  |  | ||||||
|                     // if the name is used by another process, 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 (!mMission.running || Thread.interrupted()) return; |                 if (!mMission.running || Thread.interrupted()) return; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ package us.shandian.giga.get; | |||||||
|  |  | ||||||
| import android.os.Handler; | import android.os.Handler; | ||||||
| import android.os.Message; | import android.os.Message; | ||||||
|  | import android.support.annotation.NonNull; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
|  |  | ||||||
| import java.io.File; | import java.io.File; | ||||||
| @@ -17,6 +18,7 @@ import java.util.List; | |||||||
|  |  | ||||||
| import javax.net.ssl.SSLException; | import javax.net.ssl.SSLException; | ||||||
|  |  | ||||||
|  | import us.shandian.giga.io.StoredFileHelper; | ||||||
| import us.shandian.giga.postprocessing.Postprocessing; | import us.shandian.giga.postprocessing.Postprocessing; | ||||||
| import us.shandian.giga.service.DownloadManagerService; | import us.shandian.giga.service.DownloadManagerService; | ||||||
| import us.shandian.giga.util.Utility; | import us.shandian.giga.util.Utility; | ||||||
| @@ -24,7 +26,7 @@ import us.shandian.giga.util.Utility; | |||||||
| import static org.schabi.newpipe.BuildConfig.DEBUG; | import static org.schabi.newpipe.BuildConfig.DEBUG; | ||||||
|  |  | ||||||
| public class DownloadMission extends Mission { | public class DownloadMission extends Mission { | ||||||
|     private static final long serialVersionUID = 3L;// last bump: 8 november 2018 |     private static final long serialVersionUID = 4L;// last bump: 27 march 2019 | ||||||
|  |  | ||||||
|     static final int BUFFER_SIZE = 64 * 1024; |     static final int BUFFER_SIZE = 64 * 1024; | ||||||
|     final static int BLOCK_SIZE = 512 * 1024; |     final static int BLOCK_SIZE = 512 * 1024; | ||||||
| @@ -43,6 +45,7 @@ public class DownloadMission extends Mission { | |||||||
|     public static final int ERROR_POSTPROCESSING_STOPPED = 1008; |     public static final int ERROR_POSTPROCESSING_STOPPED = 1008; | ||||||
|     public static final int ERROR_POSTPROCESSING_HOLD = 1009; |     public static final int ERROR_POSTPROCESSING_HOLD = 1009; | ||||||
|     public static final int ERROR_INSUFFICIENT_STORAGE = 1010; |     public static final int ERROR_INSUFFICIENT_STORAGE = 1010; | ||||||
|  |     public static final int ERROR_PROGRESS_LOST = 1011; | ||||||
|     public static final int ERROR_HTTP_NO_CONTENT = 204; |     public static final int ERROR_HTTP_NO_CONTENT = 204; | ||||||
|     public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206; |     public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206; | ||||||
|  |  | ||||||
| @@ -71,16 +74,6 @@ public class DownloadMission extends Mission { | |||||||
|      */ |      */ | ||||||
|     public long[] offsets; |     public long[] offsets; | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * The post-processing algorithm arguments |  | ||||||
|      */ |  | ||||||
|     public String[] postprocessingArgs; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * The post-processing algorithm name |  | ||||||
|      */ |  | ||||||
|     public String postprocessingName; |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Indicates if the post-processing state: |      * Indicates if the post-processing state: | ||||||
|      * 0: ready |      * 0: ready | ||||||
| @@ -88,12 +81,12 @@ public class DownloadMission extends Mission { | |||||||
|      * 2: completed |      * 2: completed | ||||||
|      * 3: hold |      * 3: hold | ||||||
|      */ |      */ | ||||||
|     public volatile int postprocessingState; |     public volatile int psState; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Indicate if the post-processing algorithm works on the same file |      * the post-processing algorithm instance | ||||||
|      */ |      */ | ||||||
|     public boolean postprocessingThis; |     public transient Postprocessing psAlgorithm; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The current resource to download, see {@code urls[current]} and {@code offsets[current]} |      * The current resource to download, see {@code urls[current]} and {@code offsets[current]} | ||||||
| @@ -138,36 +131,23 @@ public class DownloadMission extends Mission { | |||||||
|     public transient volatile Thread[] threads = new Thread[0]; |     public transient volatile Thread[] threads = new Thread[0]; | ||||||
|     private transient Thread init = null; |     private transient Thread init = null; | ||||||
|  |  | ||||||
|  |  | ||||||
|     protected DownloadMission() { |     protected DownloadMission() { | ||||||
|  |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public DownloadMission(String url, String name, String location, char kind) { |     public DownloadMission(String[] urls, StoredFileHelper storage, char kind, Postprocessing psInstance) { | ||||||
|         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 (urls == null) throw new NullPointerException("urls is null"); |         if (urls == null) throw new NullPointerException("urls is null"); | ||||||
|         if (urls.length < 1) throw new IllegalArgumentException("urls is empty"); |         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.urls = urls; |         this.urls = urls; | ||||||
|         this.name = name; |  | ||||||
|         this.location = location; |  | ||||||
|         this.kind = kind; |         this.kind = kind; | ||||||
|         this.offsets = new long[urls.length]; |         this.offsets = new long[urls.length]; | ||||||
|         this.enqueued = true; |         this.enqueued = true; | ||||||
|         this.maxRetry = 3; |         this.maxRetry = 3; | ||||||
|  |         this.storage = storage; | ||||||
|  |  | ||||||
|         if (postprocessingName != null) { |         if (psInstance != null) { | ||||||
|             Postprocessing algorithm = Postprocessing.getAlgorithm(postprocessingName, null); |             this.psAlgorithm = psInstance; | ||||||
|             this.postprocessingThis = algorithm.worksOnSameFile; |             this.offsets[0] = psInstance.recommendedReserve; | ||||||
|             this.offsets[0] = algorithm.recommendedReserve; |  | ||||||
|             this.postprocessingName = postprocessingName; |  | ||||||
|             this.postprocessingArgs = postprocessingArgs; |  | ||||||
|         } else { |         } else { | ||||||
|             if (DEBUG && urls.length > 1) { |             if (DEBUG && urls.length > 1) { | ||||||
|                 Log.w(TAG, "mission created with multiple urls ¿missing post-processing algorithm?"); |                 Log.w(TAG, "mission created with multiple urls ¿missing post-processing algorithm?"); | ||||||
| @@ -359,22 +339,12 @@ public class DownloadMission extends Mission { | |||||||
|         Log.e(TAG, "notifyError() code = " + code, err); |         Log.e(TAG, "notifyError() code = " + code, err); | ||||||
|  |  | ||||||
|         if (err instanceof IOException) { |         if (err instanceof IOException) { | ||||||
|             if (err.getMessage().contains("Permission denied")) { |             if (storage.canWrite() || err.getMessage().contains("Permission denied")) { | ||||||
|                 code = ERROR_PERMISSION_DENIED; |                 code = ERROR_PERMISSION_DENIED; | ||||||
|                 err = null; |                 err = null; | ||||||
|             } else if (err.getMessage().contains("write failed: ENOSPC")) { |             } else if (err.getMessage().contains("ENOSPC")) { | ||||||
|                 code = ERROR_INSUFFICIENT_STORAGE; |                 code = ERROR_INSUFFICIENT_STORAGE; | ||||||
|                 err = null; |                 err = null; | ||||||
|             } else { |  | ||||||
|                 try { |  | ||||||
|                     File storage = new File(location); |  | ||||||
|                     if (storage.canWrite() && storage.getUsableSpace() < (getLength() - done)) { |  | ||||||
|                         code = ERROR_INSUFFICIENT_STORAGE; |  | ||||||
|                         err = null; |  | ||||||
|                     } |  | ||||||
|                 } catch (SecurityException e) { |  | ||||||
|                     // is a permission error |  | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -433,11 +403,11 @@ public class DownloadMission extends Mission { | |||||||
|                 action = "Failed"; |                 action = "Failed"; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         Log.d(TAG, action + " postprocessing on " + location + File.separator + name); |         Log.d(TAG, action + " postprocessing on " + storage.getName()); | ||||||
|  |  | ||||||
|         synchronized (blockState) { |         synchronized (blockState) { | ||||||
|             // don't return without fully write the current state |             // don't return without fully write the current state | ||||||
|             postprocessingState = state; |             psState = state; | ||||||
|             Utility.writeToFile(metadata, DownloadMission.this); |             Utility.writeToFile(metadata, DownloadMission.this); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -456,7 +426,7 @@ public class DownloadMission extends Mission { | |||||||
|         running = true; |         running = true; | ||||||
|         errCode = ERROR_NOTHING; |         errCode = ERROR_NOTHING; | ||||||
|  |  | ||||||
|         if (current >= urls.length && postprocessingName != null) { |         if (current >= urls.length && psAlgorithm != null) { | ||||||
|             runAsync(1, () -> { |             runAsync(1, () -> { | ||||||
|                 if (doPostprocessing()) { |                 if (doPostprocessing()) { | ||||||
|                     running = false; |                     running = false; | ||||||
| @@ -593,7 +563,7 @@ public class DownloadMission extends Mission { | |||||||
|      * @return true, otherwise, false |      * @return true, otherwise, false | ||||||
|      */ |      */ | ||||||
|     public boolean isFinished() { |     public boolean isFinished() { | ||||||
|         return current >= urls.length && (postprocessingName == null || postprocessingState == 2); |         return current >= urls.length && (psAlgorithm == null || psState == 2); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -602,7 +572,13 @@ public class DownloadMission extends Mission { | |||||||
|      * @return {@code true} if this mission is unrecoverable |      * @return {@code true} if this mission is unrecoverable | ||||||
|      */ |      */ | ||||||
|     public boolean isPsFailed() { |     public boolean isPsFailed() { | ||||||
|         return postprocessingName != null && errCode == DownloadMission.ERROR_POSTPROCESSING && postprocessingThis; |         switch (errCode) { | ||||||
|  |             case ERROR_POSTPROCESSING: | ||||||
|  |             case ERROR_POSTPROCESSING_STOPPED: | ||||||
|  |                 return psAlgorithm.worksOnSameFile; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -611,7 +587,7 @@ public class DownloadMission extends Mission { | |||||||
|      * @return true, otherwise, false |      * @return true, otherwise, false | ||||||
|      */ |      */ | ||||||
|     public boolean isPsRunning() { |     public boolean isPsRunning() { | ||||||
|         return postprocessingName != null && (postprocessingState == 1 || postprocessingState == 3); |         return psAlgorithm != null && (psState == 1 || psState == 3); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -625,7 +601,7 @@ public class DownloadMission extends Mission { | |||||||
|  |  | ||||||
|     public long getLength() { |     public long getLength() { | ||||||
|         long calculated; |         long calculated; | ||||||
|         if (postprocessingState == 1 || postprocessingState == 3) { |         if (psState == 1 || psState == 3) { | ||||||
|             calculated = length; |             calculated = length; | ||||||
|         } else { |         } else { | ||||||
|             calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length; |             calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length; | ||||||
| @@ -652,38 +628,60 @@ public class DownloadMission extends Mission { | |||||||
|      * @param recover {@code true} to retry, otherwise, {@code false} to cancel |      * @param recover {@code true} to retry, otherwise, {@code false} to cancel | ||||||
|      */ |      */ | ||||||
|     public void psContinue(boolean recover) { |     public void psContinue(boolean recover) { | ||||||
|         postprocessingState = 1; |         psState = 1; | ||||||
|         errCode = recover ? ERROR_NOTHING : ERROR_POSTPROCESSING; |         errCode = recover ? ERROR_NOTHING : ERROR_POSTPROCESSING; | ||||||
|         threads[0].interrupt(); |         threads[0].interrupt(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * changes the StoredFileHelper for another and saves the changes to the metadata file | ||||||
|  |      * | ||||||
|  |      * @param newStorage the new StoredFileHelper instance to use | ||||||
|  |      */ | ||||||
|  |     public void changeStorage(@NonNull StoredFileHelper newStorage) { | ||||||
|  |         storage = newStorage; | ||||||
|  |         // commit changes on the metadata file | ||||||
|  |         runAsync(-2, this::writeThisToFile); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Indicates whatever the backed storage is invalid | ||||||
|  |      * | ||||||
|  |      * @return {@code true}, if storage is invalid and cannot be used | ||||||
|  |      */ | ||||||
|  |     public boolean hasInvalidStorage() { | ||||||
|  |         return errCode == ERROR_PROGRESS_LOST || storage == null || storage.isInvalid(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Indicates whatever is possible to start the mission | ||||||
|  |      * | ||||||
|  |      * @return {@code true} is this mission is "sane", otherwise, {@code false} | ||||||
|  |      */ | ||||||
|  |     public boolean canDownload() { | ||||||
|  |         return !(isPsFailed() || errCode == ERROR_POSTPROCESSING_HOLD) && !isFinished() && !hasInvalidStorage(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     private boolean doPostprocessing() { |     private boolean doPostprocessing() { | ||||||
|         if (postprocessingName == null || postprocessingState == 2) return true; |         if (psAlgorithm == null || psState == 2) return true; | ||||||
|  |  | ||||||
|         notifyPostProcessing(1); |         notifyPostProcessing(1); | ||||||
|         notifyProgress(0); |         notifyProgress(0); | ||||||
|  |  | ||||||
|         if (DEBUG) |         if (DEBUG) | ||||||
|             Thread.currentThread().setName("[" + TAG + "]  post-processing = " + postprocessingName + "  filename = " + name); |             Thread.currentThread().setName("[" + TAG + "]  ps = " + | ||||||
|  |                     psAlgorithm.getClass().getSimpleName() + | ||||||
|  |                     "  filename = " + storage.getName() | ||||||
|  |             ); | ||||||
|  |  | ||||||
|         threads = new Thread[]{Thread.currentThread()}; |         threads = new Thread[]{Thread.currentThread()}; | ||||||
|  |  | ||||||
|         Exception exception = null; |         Exception exception = null; | ||||||
|  |  | ||||||
|         try { |         try { | ||||||
|             Postprocessing |             psAlgorithm.run(this); | ||||||
|                     .getAlgorithm(postprocessingName, this) |  | ||||||
|                     .run(); |  | ||||||
|         } catch (Exception err) { |         } catch (Exception err) { | ||||||
|             StringBuilder args = new StringBuilder("  "); |             Log.e(TAG, "Post-processing failed. " + psAlgorithm.toString(), err); | ||||||
|             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); |  | ||||||
|  |  | ||||||
|             if (errCode == ERROR_NOTHING) errCode = ERROR_POSTPROCESSING; |             if (errCode == ERROR_NOTHING) errCode = ERROR_POSTPROCESSING; | ||||||
|  |  | ||||||
| @@ -733,7 +731,7 @@ public class DownloadMission extends Mission { | |||||||
|         //  >=1:     any download thread |         //  >=1:     any download thread | ||||||
|  |  | ||||||
|         if (DEBUG) { |         if (DEBUG) { | ||||||
|             who.setName(String.format("%s[%s] %s", TAG, id, name)); |             who.setName(String.format("%s[%s] %s", TAG, id, storage.getName())); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         who.start(); |         who.start(); | ||||||
|   | |||||||
| @@ -2,9 +2,10 @@ package us.shandian.giga.get; | |||||||
|  |  | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
|  |  | ||||||
| import java.io.FileNotFoundException; | import org.schabi.newpipe.streams.io.SharpStream; | ||||||
|  |  | ||||||
|  | import java.io.IOException; | ||||||
| import java.io.InputStream; | import java.io.InputStream; | ||||||
| import java.io.RandomAccessFile; |  | ||||||
| import java.net.HttpURLConnection; | import java.net.HttpURLConnection; | ||||||
| import java.nio.channels.ClosedByInterruptException; | import java.nio.channels.ClosedByInterruptException; | ||||||
|  |  | ||||||
| @@ -40,12 +41,12 @@ public class DownloadRunnable extends Thread { | |||||||
|             Log.d(TAG, mId + ":recovered: " + mMission.recovered); |             Log.d(TAG, mId + ":recovered: " + mMission.recovered); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         RandomAccessFile f; |         SharpStream f; | ||||||
|         InputStream is = null; |         InputStream is = null; | ||||||
|  |  | ||||||
|         try { |         try { | ||||||
|             f = new RandomAccessFile(mMission.getDownloadedFile(), "rw"); |             f = mMission.storage.getStream(); | ||||||
|         } catch (FileNotFoundException e) { |         } catch (IOException e) { | ||||||
|             mMission.notifyError(e);// this never should happen |             mMission.notifyError(e);// this never should happen | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -4,13 +4,13 @@ import android.annotation.SuppressLint; | |||||||
| import android.support.annotation.NonNull; | import android.support.annotation.NonNull; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.streams.io.SharpStream; | ||||||
|  |  | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.io.InputStream; | import java.io.InputStream; | ||||||
| import java.io.RandomAccessFile; |  | ||||||
| import java.net.HttpURLConnection; | import java.net.HttpURLConnection; | ||||||
| import java.nio.channels.ClosedByInterruptException; | import java.nio.channels.ClosedByInterruptException; | ||||||
|  |  | ||||||
|  |  | ||||||
| import us.shandian.giga.util.Utility; | import us.shandian.giga.util.Utility; | ||||||
|  |  | ||||||
| import static org.schabi.newpipe.BuildConfig.DEBUG; | import static org.schabi.newpipe.BuildConfig.DEBUG; | ||||||
| @@ -22,11 +22,10 @@ public class DownloadRunnableFallback extends Thread { | |||||||
|     private static final String TAG = "DownloadRunnableFallback"; |     private static final String TAG = "DownloadRunnableFallback"; | ||||||
|  |  | ||||||
|     private final DownloadMission mMission; |     private final DownloadMission mMission; | ||||||
|     private final int mId = 1; |  | ||||||
|  |  | ||||||
|     private int mRetryCount = 0; |     private int mRetryCount = 0; | ||||||
|     private InputStream mIs; |     private InputStream mIs; | ||||||
|     private RandomAccessFile mF; |     private SharpStream mF; | ||||||
|     private HttpURLConnection mConn; |     private HttpURLConnection mConn; | ||||||
|  |  | ||||||
|     DownloadRunnableFallback(@NonNull DownloadMission mission) { |     DownloadRunnableFallback(@NonNull DownloadMission mission) { | ||||||
| @@ -43,11 +42,7 @@ public class DownloadRunnableFallback extends Thread { | |||||||
|             // nothing to do |             // nothing to do | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         try { |  | ||||||
|         if (mF != null) mF.close(); |         if (mF != null) mF.close(); | ||||||
|         } catch (IOException e) { |  | ||||||
|             // ¿ejected media storage? ¿file deleted? ¿storage ran out of space? |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -67,6 +62,7 @@ public class DownloadRunnableFallback extends Thread { | |||||||
|         try { |         try { | ||||||
|             long rangeStart = (mMission.unknownLength || start < 1) ? -1 : start; |             long rangeStart = (mMission.unknownLength || start < 1) ? -1 : start; | ||||||
|  |  | ||||||
|  |             int mId = 1; | ||||||
|             mConn = mMission.openConnection(mId, rangeStart, -1); |             mConn = mMission.openConnection(mId, rangeStart, -1); | ||||||
|             mMission.establishConnection(mId, mConn); |             mMission.establishConnection(mId, mConn); | ||||||
|  |  | ||||||
| @@ -81,7 +77,7 @@ public class DownloadRunnableFallback extends Thread { | |||||||
|             if (!mMission.unknownLength) |             if (!mMission.unknownLength) | ||||||
|                 mMission.unknownLength = Utility.getContentLength(mConn) == -1; |                 mMission.unknownLength = Utility.getContentLength(mConn) == -1; | ||||||
|  |  | ||||||
|             mF = new RandomAccessFile(mMission.getDownloadedFile(), "rw"); |             mF = mMission.storage.getStream(); | ||||||
|             mF.seek(mMission.offsets[mMission.current] + start); |             mF.seek(mMission.offsets[mMission.current] + start); | ||||||
|  |  | ||||||
|             mIs = mConn.getInputStream(); |             mIs = mConn.getInputStream(); | ||||||
|   | |||||||
| @@ -1,16 +1,16 @@ | |||||||
| package us.shandian.giga.get; | package us.shandian.giga.get; | ||||||
|  |  | ||||||
|  | import android.support.annotation.NonNull; | ||||||
|  |  | ||||||
| public class FinishedMission extends  Mission { | public class FinishedMission extends  Mission { | ||||||
|  |  | ||||||
|     public FinishedMission() { |     public FinishedMission() { | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public FinishedMission(DownloadMission mission) { |     public FinishedMission(@NonNull DownloadMission mission) { | ||||||
|         source = mission.source; |         source = mission.source; | ||||||
|         length = mission.length;// ¿or mission.done? |         length = mission.length;// ¿or mission.done? | ||||||
|         timestamp = mission.timestamp; |         timestamp = mission.timestamp; | ||||||
|         name = mission.name; |  | ||||||
|         location = mission.location; |  | ||||||
|         kind = mission.kind; |         kind = mission.kind; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,12 +1,15 @@ | |||||||
| package us.shandian.giga.get; | package us.shandian.giga.get; | ||||||
|  |  | ||||||
| import java.io.File; | import android.net.Uri; | ||||||
|  | import android.support.annotation.NonNull; | ||||||
|  |  | ||||||
| import java.io.Serializable; | import java.io.Serializable; | ||||||
| import java.text.SimpleDateFormat; |  | ||||||
| import java.util.Calendar; | import java.util.Calendar; | ||||||
|  |  | ||||||
|  | import us.shandian.giga.io.StoredFileHelper; | ||||||
|  |  | ||||||
| public abstract class Mission implements Serializable { | public abstract class Mission implements Serializable { | ||||||
|     private static final long serialVersionUID = 0L;// last bump: 5 october 2018 |     private static final long serialVersionUID = 1L;// last bump: 27 march 2019 | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Source url of the resource |      * Source url of the resource | ||||||
| @@ -23,28 +26,23 @@ public abstract class Mission implements Serializable { | |||||||
|      */ |      */ | ||||||
|     public long timestamp; |     public long timestamp; | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * The filename |  | ||||||
|      */ |  | ||||||
|     public String name; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * The directory to store the download |  | ||||||
|      */ |  | ||||||
|     public String location; |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * pre-defined content type |      * pre-defined content type | ||||||
|      */ |      */ | ||||||
|     public char kind; |     public char kind; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The downloaded file | ||||||
|  |      */ | ||||||
|  |     public StoredFileHelper storage; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * get the target file on the storage |      * get the target file on the storage | ||||||
|      * |      * | ||||||
|      * @return File object |      * @return File object | ||||||
|      */ |      */ | ||||||
|     public File getDownloadedFile() { |     public Uri getDownloadedFileUri() { | ||||||
|         return new File(location, name); |         return storage.getUri(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -53,8 +51,8 @@ public abstract class Mission implements Serializable { | |||||||
|      * @return {@code true] if and only if the file is successfully deleted, otherwise, {@code false} |      * @return {@code true] if and only if the file is successfully deleted, otherwise, {@code false} | ||||||
|      */ |      */ | ||||||
|     public boolean delete() { |     public boolean delete() { | ||||||
|         deleted = true; |         if (storage != null) return storage.delete(); | ||||||
|         return getDownloadedFile().delete(); |         return  true; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -62,10 +60,11 @@ public abstract class Mission implements Serializable { | |||||||
|      */ |      */ | ||||||
|     public transient boolean deleted = false; |     public transient boolean deleted = false; | ||||||
|  |  | ||||||
|  |     @NonNull | ||||||
|     @Override |     @Override | ||||||
|     public String toString() { |     public String toString() { | ||||||
|         Calendar calendar = Calendar.getInstance(); |         Calendar calendar = Calendar.getInstance(); | ||||||
|         calendar.setTimeInMillis(timestamp); |         calendar.setTimeInMillis(timestamp); | ||||||
|         return "[" + calendar.getTime().toString() + "] " + location + File.separator + name; |         return "[" + calendar.getTime().toString() + "] " + getDownloadedFileUri().getPath(); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,73 +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.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); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,112 +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.database.sqlite.SQLiteOpenHelper; |  | ||||||
|  |  | ||||||
| import us.shandian.giga.get.DownloadMission; |  | ||||||
| import us.shandian.giga.get.FinishedMission; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * SQLiteHelper to store finished {@link us.shandian.giga.get.DownloadMission}'s |  | ||||||
|  */ |  | ||||||
| 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 = 3; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * The table name of download missions |  | ||||||
|      */ |  | ||||||
|     static final String MISSIONS_TABLE_NAME = "download_missions"; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * The key to the directory location of the mission |  | ||||||
|      */ |  | ||||||
|     static final String KEY_LOCATION = "location"; |  | ||||||
|     /** |  | ||||||
|      * The key to the urls of a mission |  | ||||||
|      */ |  | ||||||
|     static final String KEY_SOURCE_URL = "url"; |  | ||||||
|     /** |  | ||||||
|      * The key to the name of a mission |  | ||||||
|      */ |  | ||||||
|     static final String KEY_NAME = "name"; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * The key to the done. |  | ||||||
|      */ |  | ||||||
|     static final String KEY_DONE = "bytes_downloaded"; |  | ||||||
|  |  | ||||||
|     static final String KEY_TIMESTAMP = "timestamp"; |  | ||||||
|  |  | ||||||
|     static  final String KEY_KIND = "kind"; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * The statement to create the table |  | ||||||
|      */ |  | ||||||
|     private static final String MISSIONS_CREATE_TABLE = |  | ||||||
|             "CREATE TABLE " + MISSIONS_TABLE_NAME + " (" + |  | ||||||
|                     KEY_LOCATION + " TEXT NOT NULL, " + |  | ||||||
|                     KEY_NAME + " 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 + "));"; |  | ||||||
|  |  | ||||||
|     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. |  | ||||||
|      * |  | ||||||
|      * @param downloadMission the download mission |  | ||||||
|      * @return the content values |  | ||||||
|      */ |  | ||||||
|     public static ContentValues getValuesOfMission(DownloadMission downloadMission) { |  | ||||||
|         ContentValues values = new ContentValues(); |  | ||||||
|         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; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static FinishedMission getMissionFromCursor(Cursor cursor) { |  | ||||||
|         if (cursor == null) throw new NullPointerException("cursor is null"); |  | ||||||
|  |  | ||||||
|         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.kind = kind.charAt(0); |  | ||||||
|  |  | ||||||
|         return mission; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,223 @@ | |||||||
|  | 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.database.sqlite.SQLiteOpenHelper; | ||||||
|  | import android.net.Uri; | ||||||
|  | import android.support.annotation.NonNull; | ||||||
|  | import android.util.Log; | ||||||
|  |  | ||||||
|  | import java.io.File; | ||||||
|  | import java.util.ArrayList; | ||||||
|  |  | ||||||
|  | import us.shandian.giga.get.DownloadMission; | ||||||
|  | import us.shandian.giga.get.FinishedMission; | ||||||
|  | import us.shandian.giga.get.Mission; | ||||||
|  | import us.shandian.giga.io.StoredFileHelper; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * SQLite helper to store finished {@link us.shandian.giga.get.FinishedMission}'s | ||||||
|  |  */ | ||||||
|  | public class FinishedMissionStore extends SQLiteOpenHelper { | ||||||
|  |  | ||||||
|  |     // TODO: use NewPipeSQLiteHelper ('s constants) when playlist branch is merged (?) | ||||||
|  |     private static final String DATABASE_NAME = "downloads.db"; | ||||||
|  |  | ||||||
|  |     private static final int DATABASE_VERSION = 4; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The table name of download missions (old) | ||||||
|  |      */ | ||||||
|  |     private static final String MISSIONS_TABLE_NAME_v2 = "download_missions"; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The table name of download missions | ||||||
|  |      */ | ||||||
|  |     private static final String FINISHED_MISSIONS_TABLE_NAME = "finished_missions"; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The key to the urls of a mission | ||||||
|  |      */ | ||||||
|  |     private static final String KEY_SOURCE = "url"; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The key to the done. | ||||||
|  |      */ | ||||||
|  |     private static final String KEY_DONE = "bytes_downloaded"; | ||||||
|  |  | ||||||
|  |     private static final String KEY_TIMESTAMP = "timestamp"; | ||||||
|  |  | ||||||
|  |     private static final String KEY_KIND = "kind"; | ||||||
|  |  | ||||||
|  |     private static final String KEY_PATH = "path"; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The statement to create the table | ||||||
|  |      */ | ||||||
|  |     private static final String MISSIONS_CREATE_TABLE = | ||||||
|  |             "CREATE TABLE " + FINISHED_MISSIONS_TABLE_NAME + " (" + | ||||||
|  |                     KEY_PATH + " TEXT NOT NULL, " + | ||||||
|  |                     KEY_SOURCE + " TEXT NOT NULL, " + | ||||||
|  |                     KEY_DONE + " INTEGER NOT NULL, " + | ||||||
|  |                     KEY_TIMESTAMP + " INTEGER NOT NULL, " + | ||||||
|  |                     KEY_KIND + " TEXT NOT NULL, " + | ||||||
|  |                     " UNIQUE(" + KEY_TIMESTAMP + ", " + KEY_PATH + "));"; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     private Context context; | ||||||
|  |  | ||||||
|  |     public FinishedMissionStore(Context context) { | ||||||
|  |         super(context, DATABASE_NAME, null, DATABASE_VERSION); | ||||||
|  |         this.context = context; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @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_v2 + " ADD COLUMN " + KEY_KIND + " TEXT;"); | ||||||
|  |             oldVersion++; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (oldVersion == 3) { | ||||||
|  |             final String KEY_LOCATION = "location"; | ||||||
|  |             final String KEY_NAME = "name"; | ||||||
|  |  | ||||||
|  |             db.execSQL(MISSIONS_CREATE_TABLE); | ||||||
|  |  | ||||||
|  |             Cursor cursor = db.query(MISSIONS_TABLE_NAME_v2, null, null, | ||||||
|  |                     null, null, null, KEY_TIMESTAMP); | ||||||
|  |  | ||||||
|  |             int count = cursor.getCount(); | ||||||
|  |             if (count > 0) { | ||||||
|  |                 db.beginTransaction(); | ||||||
|  |                 while (cursor.moveToNext()) { | ||||||
|  |                     ContentValues values = new ContentValues(); | ||||||
|  |                     values.put(KEY_SOURCE, cursor.getString(cursor.getColumnIndex(KEY_SOURCE))); | ||||||
|  |                     values.put(KEY_DONE, cursor.getString(cursor.getColumnIndex(KEY_DONE))); | ||||||
|  |                     values.put(KEY_TIMESTAMP, cursor.getLong(cursor.getColumnIndex(KEY_TIMESTAMP))); | ||||||
|  |                     values.put(KEY_KIND, cursor.getString(cursor.getColumnIndex(KEY_KIND))); | ||||||
|  |                     values.put(KEY_PATH, Uri.fromFile( | ||||||
|  |                             new File( | ||||||
|  |                                     cursor.getString(cursor.getColumnIndex(KEY_LOCATION)), | ||||||
|  |                                     cursor.getString(cursor.getColumnIndex(KEY_NAME)) | ||||||
|  |                             ) | ||||||
|  |                     ).toString()); | ||||||
|  |  | ||||||
|  |                     db.insert(FINISHED_MISSIONS_TABLE_NAME, null, values); | ||||||
|  |                 } | ||||||
|  |                 db.setTransactionSuccessful(); | ||||||
|  |                 db.endTransaction(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             cursor.close(); | ||||||
|  |             db.execSQL("DROP TABLE " + MISSIONS_TABLE_NAME_v2); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns all values of the download mission as ContentValues. | ||||||
|  |      * | ||||||
|  |      * @param downloadMission the download mission | ||||||
|  |      * @return the content values | ||||||
|  |      */ | ||||||
|  |     private ContentValues getValuesOfMission(@NonNull Mission downloadMission) { | ||||||
|  |         ContentValues values = new ContentValues(); | ||||||
|  |         values.put(KEY_SOURCE, downloadMission.source); | ||||||
|  |         values.put(KEY_PATH, downloadMission.storage.getUri().toString()); | ||||||
|  |         values.put(KEY_DONE, downloadMission.length); | ||||||
|  |         values.put(KEY_TIMESTAMP, downloadMission.timestamp); | ||||||
|  |         values.put(KEY_KIND, String.valueOf(downloadMission.kind)); | ||||||
|  |         return values; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private FinishedMission getMissionFromCursor(Cursor cursor) { | ||||||
|  |         if (cursor == null) throw new NullPointerException("cursor is null"); | ||||||
|  |  | ||||||
|  |         String kind = cursor.getString(cursor.getColumnIndex(KEY_KIND)); | ||||||
|  |         if (kind == null || kind.isEmpty()) kind = "?"; | ||||||
|  |  | ||||||
|  |         String path = cursor.getString(cursor.getColumnIndexOrThrow(KEY_PATH)); | ||||||
|  |  | ||||||
|  |         FinishedMission mission = new FinishedMission(); | ||||||
|  |  | ||||||
|  |         mission.source = cursor.getString(cursor.getColumnIndexOrThrow(KEY_SOURCE)); | ||||||
|  |         mission.length = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE)); | ||||||
|  |         mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP)); | ||||||
|  |         mission.kind = kind.charAt(0); | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             mission.storage = new StoredFileHelper(context, Uri.parse(path), ""); | ||||||
|  |         } catch (Exception e) { | ||||||
|  |             Log.e("FinishedMissionStore", "failed to load the storage path of: " + path, e); | ||||||
|  |             mission.storage = new StoredFileHelper(path, "", ""); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return mission; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     ////////////////////////////////// | ||||||
|  |     // Data source methods | ||||||
|  |     /////////////////////////////////// | ||||||
|  |  | ||||||
|  |     public ArrayList<FinishedMission> loadFinishedMissions() { | ||||||
|  |         SQLiteDatabase database = getReadableDatabase(); | ||||||
|  |         Cursor cursor = database.query(FINISHED_MISSIONS_TABLE_NAME, null, null, | ||||||
|  |                 null, null, null, KEY_TIMESTAMP + " DESC"); | ||||||
|  |  | ||||||
|  |         int count = cursor.getCount(); | ||||||
|  |         if (count == 0) return new ArrayList<>(1); | ||||||
|  |  | ||||||
|  |         ArrayList<FinishedMission> result = new ArrayList<>(count); | ||||||
|  |         while (cursor.moveToNext()) { | ||||||
|  |             result.add(getMissionFromCursor(cursor)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void addFinishedMission(DownloadMission downloadMission) { | ||||||
|  |         if (downloadMission == null) throw new NullPointerException("downloadMission is null"); | ||||||
|  |         SQLiteDatabase database = getWritableDatabase(); | ||||||
|  |         ContentValues values = getValuesOfMission(downloadMission); | ||||||
|  |         database.insert(FINISHED_MISSIONS_TABLE_NAME, null, values); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void deleteMission(Mission mission) { | ||||||
|  |         if (mission == null) throw new NullPointerException("mission is null"); | ||||||
|  |         String path = mission.getDownloadedFileUri().toString(); | ||||||
|  |  | ||||||
|  |         SQLiteDatabase database = getWritableDatabase(); | ||||||
|  |  | ||||||
|  |         if (mission instanceof FinishedMission) | ||||||
|  |             database.delete(FINISHED_MISSIONS_TABLE_NAME, KEY_TIMESTAMP + " = ?, " + KEY_PATH + " = ?", new String[]{path}); | ||||||
|  |         else | ||||||
|  |             throw new UnsupportedOperationException("DownloadMission"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void updateMission(Mission mission) { | ||||||
|  |         if (mission == null) throw new NullPointerException("mission is null"); | ||||||
|  |         SQLiteDatabase database = getWritableDatabase(); | ||||||
|  |         ContentValues values = getValuesOfMission(mission); | ||||||
|  |         String path = mission.getDownloadedFileUri().toString(); | ||||||
|  |  | ||||||
|  |         int rowsAffected; | ||||||
|  |  | ||||||
|  |         if (mission instanceof FinishedMission) | ||||||
|  |             rowsAffected = database.update(FINISHED_MISSIONS_TABLE_NAME, values, KEY_PATH + " = ?", new String[]{path}); | ||||||
|  |         else | ||||||
|  |             throw new UnsupportedOperationException("DownloadMission"); | ||||||
|  |  | ||||||
|  |         if (rowsAffected != 1) { | ||||||
|  |             Log.e("FinishedMissionStore", "Expected 1 row to be affected by update but got " + rowsAffected); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,20 +1,22 @@ | |||||||
| package us.shandian.giga.postprocessing.io; | package us.shandian.giga.io; | ||||||
| 
 | 
 | ||||||
| import org.schabi.newpipe.streams.io.SharpStream; | import org.schabi.newpipe.streams.io.SharpStream; | ||||||
| 
 | 
 | ||||||
| import java.io.File; |  | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.io.RandomAccessFile; |  | ||||||
| 
 | 
 | ||||||
| public class ChunkFileInputStream extends SharpStream { | public class ChunkFileInputStream extends SharpStream { | ||||||
| 
 | 
 | ||||||
|     private RandomAccessFile source; |     private SharpStream source; | ||||||
|     private final long offset; |     private final long offset; | ||||||
|     private final long length; |     private final long length; | ||||||
|     private long position; |     private long position; | ||||||
| 
 | 
 | ||||||
|     public ChunkFileInputStream(File file, long start, long end, String mode) throws IOException { |     public ChunkFileInputStream(SharpStream target, long start) throws IOException { | ||||||
|         source = new RandomAccessFile(file, mode); |         this(target, start, target.length()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public ChunkFileInputStream(SharpStream target, long start, long end) throws IOException { | ||||||
|  |         source = target; | ||||||
|         offset = start; |         offset = start; | ||||||
|         length = end - start; |         length = end - start; | ||||||
|         position = 0; |         position = 0; | ||||||
| @@ -100,17 +102,13 @@ public class ChunkFileInputStream extends SharpStream { | |||||||
| 
 | 
 | ||||||
|     @SuppressWarnings("EmptyCatchBlock") |     @SuppressWarnings("EmptyCatchBlock") | ||||||
|     @Override |     @Override | ||||||
|     public void dispose() { |     public void close() { | ||||||
|         try { |  | ||||||
|         source.close(); |         source.close(); | ||||||
|         } catch (IOException err) { |  | ||||||
|         } finally { |  | ||||||
|         source = null; |         source = null; | ||||||
|     } |     } | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public boolean isDisposed() { |     public boolean isClosed() { | ||||||
|         return source == null; |         return source == null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| package us.shandian.giga.postprocessing.io; | package us.shandian.giga.io; | ||||||
| 
 | 
 | ||||||
| import android.support.annotation.NonNull; | import android.support.annotation.NonNull; | ||||||
| 
 | 
 | ||||||
| @@ -7,7 +7,6 @@ import org.schabi.newpipe.streams.io.SharpStream; | |||||||
| import java.io.File; | import java.io.File; | ||||||
| import java.io.FileNotFoundException; | import java.io.FileNotFoundException; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.io.RandomAccessFile; |  | ||||||
| 
 | 
 | ||||||
| public class CircularFileWriter extends SharpStream { | public class CircularFileWriter extends SharpStream { | ||||||
| 
 | 
 | ||||||
| @@ -26,7 +25,7 @@ public class CircularFileWriter extends SharpStream { | |||||||
|     private BufferedFile out; |     private BufferedFile out; | ||||||
|     private BufferedFile aux; |     private BufferedFile aux; | ||||||
| 
 | 
 | ||||||
|     public CircularFileWriter(File source, File temp, OffsetChecker checker) throws IOException { |     public CircularFileWriter(SharpStream target, File temp, OffsetChecker checker) throws IOException { | ||||||
|         if (checker == null) { |         if (checker == null) { | ||||||
|             throw new NullPointerException("checker is null"); |             throw new NullPointerException("checker is null"); | ||||||
|         } |         } | ||||||
| @@ -38,7 +37,7 @@ public class CircularFileWriter extends SharpStream { | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         aux = new BufferedFile(temp); |         aux = new BufferedFile(temp); | ||||||
|         out = new BufferedFile(source); |         out = new BufferedFile(target); | ||||||
| 
 | 
 | ||||||
|         callback = checker; |         callback = checker; | ||||||
| 
 | 
 | ||||||
| @@ -105,7 +104,7 @@ public class CircularFileWriter extends SharpStream { | |||||||
|             out.target.setLength(length); |             out.target.setLength(length); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         dispose(); |         close(); | ||||||
| 
 | 
 | ||||||
|         return length; |         return length; | ||||||
|     } |     } | ||||||
| @@ -114,13 +113,13 @@ public class CircularFileWriter extends SharpStream { | |||||||
|      * Close the file without flushing any buffer |      * Close the file without flushing any buffer | ||||||
|      */ |      */ | ||||||
|     @Override |     @Override | ||||||
|     public void dispose() { |     public void close() { | ||||||
|         if (out != null) { |         if (out != null) { | ||||||
|             out.dispose(); |             out.close(); | ||||||
|             out = null; |             out = null; | ||||||
|         } |         } | ||||||
|         if (aux != null) { |         if (aux != null) { | ||||||
|             aux.dispose(); |             aux.close(); | ||||||
|             aux = null; |             aux = null; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -256,7 +255,7 @@ public class CircularFileWriter extends SharpStream { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public boolean isDisposed() { |     public boolean isClosed() { | ||||||
|         return out == null; |         return out == null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @@ -339,30 +338,29 @@ public class CircularFileWriter extends SharpStream { | |||||||
| 
 | 
 | ||||||
|     class BufferedFile { |     class BufferedFile { | ||||||
| 
 | 
 | ||||||
|         protected final RandomAccessFile target; |         protected final SharpStream target; | ||||||
| 
 | 
 | ||||||
|         private long offset; |         private long offset; | ||||||
|         protected long length; |         protected long length; | ||||||
| 
 | 
 | ||||||
|         private byte[] queue; |         private byte[] queue = new byte[QUEUE_BUFFER_SIZE]; | ||||||
|         private int queueSize; |         private int queueSize; | ||||||
| 
 | 
 | ||||||
|         BufferedFile(File file) throws FileNotFoundException { |         BufferedFile(File file) throws FileNotFoundException { | ||||||
|             queue = new byte[QUEUE_BUFFER_SIZE]; |             this.target = new FileStream(file); | ||||||
|             target = new RandomAccessFile(file, "rw"); |         } | ||||||
|  | 
 | ||||||
|  |         BufferedFile(SharpStream target) { | ||||||
|  |             this.target = target; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         protected long getOffset() { |         protected long getOffset() { | ||||||
|             return offset + queueSize;// absolute offset in the file |             return offset + queueSize;// absolute offset in the file | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         protected void dispose() { |         protected void close() { | ||||||
|             try { |  | ||||||
|             queue = null; |             queue = null; | ||||||
|             target.close(); |             target.close(); | ||||||
|             } catch (IOException e) { |  | ||||||
|                 // nothing to do |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         protected void write(byte b[], int off, int len) throws IOException { |         protected void write(byte b[], int off, int len) throws IOException { | ||||||
| @@ -384,7 +382,7 @@ public class CircularFileWriter extends SharpStream { | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         protected void flush() throws IOException { |         void flush() throws IOException { | ||||||
|             writeProof(queue, queueSize); |             writeProof(queue, queueSize); | ||||||
|             offset += queueSize; |             offset += queueSize; | ||||||
|             queueSize = 0; |             queueSize = 0; | ||||||
| @@ -404,7 +402,7 @@ public class CircularFileWriter extends SharpStream { | |||||||
|             return queue.length - queueSize; |             return queue.length - queueSize; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         protected void reset() throws IOException { |         void reset() throws IOException { | ||||||
|             offset = 0; |             offset = 0; | ||||||
|             length = 0; |             length = 0; | ||||||
|             target.seek(0); |             target.seek(0); | ||||||
| @@ -415,7 +413,7 @@ public class CircularFileWriter extends SharpStream { | |||||||
|             target.seek(absoluteOffset); |             target.seek(absoluteOffset); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         protected void writeProof(byte[] buffer, int length) throws IOException { |         void writeProof(byte[] buffer, int length) throws IOException { | ||||||
|             if (onWriteError == null) { |             if (onWriteError == null) { | ||||||
|                 target.write(buffer, 0, length); |                 target.write(buffer, 0, length); | ||||||
|                 return; |                 return; | ||||||
| @@ -436,14 +434,8 @@ public class CircularFileWriter extends SharpStream { | |||||||
|         @NonNull |         @NonNull | ||||||
|         @Override |         @Override | ||||||
|         public String toString() { |         public String toString() { | ||||||
|             String absOffset; |  | ||||||
|             String absLength; |             String absLength; | ||||||
| 
 | 
 | ||||||
|             try { |  | ||||||
|                 absOffset = Long.toString(target.getFilePointer()); |  | ||||||
|             } catch (IOException e) { |  | ||||||
|                 absOffset = "[" + e.getLocalizedMessage() + "]"; |  | ||||||
|             } |  | ||||||
|             try { |             try { | ||||||
|                 absLength = Long.toString(target.length()); |                 absLength = Long.toString(target.length()); | ||||||
|             } catch (IOException e) { |             } catch (IOException e) { | ||||||
| @@ -451,8 +443,8 @@ public class CircularFileWriter extends SharpStream { | |||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             return String.format( |             return String.format( | ||||||
|                     "offset=%s  length=%s queue=%s  absOffset=%s  absLength=%s", |                     "offset=%s  length=%s  queue=%s  absLength=%s", | ||||||
|                     offset, length, queueSize, absOffset, absLength |                     offset, length, queueSize, absLength | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
							
								
								
									
										131
									
								
								app/src/main/java/us/shandian/giga/io/FileStream.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								app/src/main/java/us/shandian/giga/io/FileStream.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,131 @@ | |||||||
|  | package us.shandian.giga.io; | ||||||
|  |  | ||||||
|  | import android.support.annotation.NonNull; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.streams.io.SharpStream; | ||||||
|  |  | ||||||
|  | import java.io.File; | ||||||
|  | import java.io.FileNotFoundException; | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.io.RandomAccessFile; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @author kapodamy | ||||||
|  |  */ | ||||||
|  | public class FileStream extends SharpStream { | ||||||
|  |  | ||||||
|  |     public RandomAccessFile source; | ||||||
|  |  | ||||||
|  |     public FileStream(@NonNull File target) throws FileNotFoundException { | ||||||
|  |         this.source = new RandomAccessFile(target, "rw"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public FileStream(@NonNull String path) throws FileNotFoundException { | ||||||
|  |         this.source = new RandomAccessFile(path, "rw"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public int read() throws IOException { | ||||||
|  |         return source.read(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public int read(byte b[]) throws IOException { | ||||||
|  |         return source.read(b); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @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 { | ||||||
|  |         return source.skipBytes((int) pos); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public long available() { | ||||||
|  |         try { | ||||||
|  |             return source.length() - source.getFilePointer(); | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             return 0; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void close() { | ||||||
|  |         if (source == null) return; | ||||||
|  |         try { | ||||||
|  |             source.close(); | ||||||
|  |         } catch (IOException err) { | ||||||
|  |             // nothing to do | ||||||
|  |         } | ||||||
|  |         source = null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean isClosed() { | ||||||
|  |         return source == null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void rewind() throws IOException { | ||||||
|  |         source.seek(0); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean canRewind() { | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean canRead() { | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean canWrite() { | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean canSeek() { | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean canSetLength() { | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @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 setLength(long length) throws IOException { | ||||||
|  |         source.setLength(length); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void seek(long offset) throws IOException { | ||||||
|  |         source.seek(offset); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public long length() throws IOException { | ||||||
|  |         return source.length(); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										140
									
								
								app/src/main/java/us/shandian/giga/io/FileStreamSAF.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								app/src/main/java/us/shandian/giga/io/FileStreamSAF.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | |||||||
|  | package us.shandian.giga.io; | ||||||
|  |  | ||||||
|  | import android.content.ContentResolver; | ||||||
|  | import android.net.Uri; | ||||||
|  | import android.os.ParcelFileDescriptor; | ||||||
|  | import android.support.annotation.NonNull; | ||||||
|  | import android.util.Log; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.streams.io.SharpStream; | ||||||
|  |  | ||||||
|  | import java.io.FileInputStream; | ||||||
|  | import java.io.FileOutputStream; | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.nio.channels.FileChannel; | ||||||
|  |  | ||||||
|  | public class FileStreamSAF extends SharpStream { | ||||||
|  |  | ||||||
|  |     private final FileInputStream in; | ||||||
|  |     private final FileOutputStream out; | ||||||
|  |     private final FileChannel channel; | ||||||
|  |     private final ParcelFileDescriptor file; | ||||||
|  |  | ||||||
|  |     private boolean disposed; | ||||||
|  |  | ||||||
|  |     public FileStreamSAF(@NonNull ContentResolver contentResolver, Uri fileUri) throws IOException { | ||||||
|  |         // Notes: | ||||||
|  |         // the file must exists first | ||||||
|  |         // ¡read-write mode must allow seek! | ||||||
|  |         // It is not guaranteed to work with files in the cloud (virtual files), tested in local storage devices | ||||||
|  |  | ||||||
|  |         file = contentResolver.openFileDescriptor(fileUri, "rw"); | ||||||
|  |  | ||||||
|  |         if (file == null) { | ||||||
|  |             throw new IOException("Cannot get the ParcelFileDescriptor for " + fileUri.toString()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         in = new FileInputStream(file.getFileDescriptor()); | ||||||
|  |         out = new FileOutputStream(file.getFileDescriptor()); | ||||||
|  |         channel = out.getChannel();// or use in.getChannel() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public int read() throws IOException { | ||||||
|  |         return in.read(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public int read(byte[] buffer) throws IOException { | ||||||
|  |         return in.read(buffer); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public int read(byte[] buffer, int offset, int count) throws IOException { | ||||||
|  |         return in.read(buffer, offset, count); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public long skip(long amount) throws IOException { | ||||||
|  |         return in.skip(amount);// ¿or use channel.position(channel.position() + amount)? | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public long available() { | ||||||
|  |         try { | ||||||
|  |             return in.available(); | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             return 0;// ¡but not -1! | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void rewind() throws IOException { | ||||||
|  |         seek(0); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void close() { | ||||||
|  |         try { | ||||||
|  |             disposed = true; | ||||||
|  |  | ||||||
|  |             file.close(); | ||||||
|  |             in.close(); | ||||||
|  |             out.close(); | ||||||
|  |             channel.close(); | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             Log.e("FileStreamSAF", "close() error", e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean isClosed() { | ||||||
|  |         return disposed; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean canRewind() { | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean canRead() { | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean canWrite() { | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public boolean canSetLength() { | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public boolean canSeek() { | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void write(byte value) throws IOException { | ||||||
|  |         out.write(value); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void write(byte[] buffer) throws IOException { | ||||||
|  |         out.write(buffer); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void write(byte[] buffer, int offset, int count) throws IOException { | ||||||
|  |         out.write(buffer, offset, count); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setLength(long length) throws IOException { | ||||||
|  |         channel.truncate(length); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void seek(long offset) throws IOException { | ||||||
|  |         channel.position(offset); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -3,7 +3,7 @@ | |||||||
|  * To change this template file, choose Tools | Templates |  * To change this template file, choose Tools | Templates | ||||||
|  * and open the template in the editor. |  * and open the template in the editor. | ||||||
|  */ |  */ | ||||||
| package us.shandian.giga.postprocessing.io; | package us.shandian.giga.io; | ||||||
| 
 | 
 | ||||||
| import android.support.annotation.NonNull; | import android.support.annotation.NonNull; | ||||||
| 
 | 
 | ||||||
| @@ -56,6 +56,6 @@ public class SharpInputStream extends InputStream { | |||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void close() { |     public void close() { | ||||||
|         base.dispose(); |         base.close(); | ||||||
|     } |     } | ||||||
| } | } | ||||||
							
								
								
									
										175
									
								
								app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,175 @@ | |||||||
|  | package us.shandian.giga.io; | ||||||
|  |  | ||||||
|  | import android.annotation.TargetApi; | ||||||
|  | import android.content.ContentResolver; | ||||||
|  | import android.content.Context; | ||||||
|  | import android.content.Intent; | ||||||
|  | import android.net.Uri; | ||||||
|  | import android.os.Build; | ||||||
|  | import android.support.annotation.NonNull; | ||||||
|  | import android.support.annotation.Nullable; | ||||||
|  | import android.support.annotation.RequiresApi; | ||||||
|  | import android.support.v4.provider.DocumentFile; | ||||||
|  |  | ||||||
|  | import java.io.File; | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Arrays; | ||||||
|  |  | ||||||
|  | public class StoredDirectoryHelper { | ||||||
|  |     public final static int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; | ||||||
|  |  | ||||||
|  |     private File ioTree; | ||||||
|  |     private DocumentFile docTree; | ||||||
|  |  | ||||||
|  |     private ContentResolver contentResolver; | ||||||
|  |  | ||||||
|  |     private String tag; | ||||||
|  |  | ||||||
|  |     @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||||
|  |     public StoredDirectoryHelper(@NonNull Context context, @NonNull Uri path, String tag) throws IOException { | ||||||
|  |         this.contentResolver = context.getContentResolver(); | ||||||
|  |         this.tag = tag; | ||||||
|  |         this.docTree = DocumentFile.fromTreeUri(context, path); | ||||||
|  |  | ||||||
|  |         if (this.docTree == null) | ||||||
|  |             throw new IOException("Failed to create the tree from Uri"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @TargetApi(Build.VERSION_CODES.KITKAT) | ||||||
|  |     public StoredDirectoryHelper(@NonNull String location, String tag) { | ||||||
|  |         ioTree = new File(location); | ||||||
|  |         this.tag = tag; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Nullable | ||||||
|  |     public StoredFileHelper createFile(String filename, String mime) { | ||||||
|  |         StoredFileHelper storage; | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             if (docTree == null) { | ||||||
|  |                 storage = new StoredFileHelper(ioTree, filename, tag); | ||||||
|  |                 storage.sourceTree = Uri.fromFile(ioTree).toString(); | ||||||
|  |             } else { | ||||||
|  |                 storage = new StoredFileHelper(docTree, contentResolver, filename, mime, tag); | ||||||
|  |                 storage.sourceTree = docTree.getUri().toString(); | ||||||
|  |             } | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         storage.tag = tag; | ||||||
|  |  | ||||||
|  |         return storage; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public StoredFileHelper createUniqueFile(String filename, String mime) { | ||||||
|  |         ArrayList<String> existingNames = new ArrayList<>(50); | ||||||
|  |  | ||||||
|  |         String ext; | ||||||
|  |  | ||||||
|  |         int dotIndex = filename.lastIndexOf('.'); | ||||||
|  |         if (dotIndex < 0 || (dotIndex == filename.length() - 1)) { | ||||||
|  |             ext = ""; | ||||||
|  |         } else { | ||||||
|  |             ext = filename.substring(dotIndex); | ||||||
|  |             filename = filename.substring(0, dotIndex - 1); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         String name; | ||||||
|  |         if (docTree == null) { | ||||||
|  |             for (File file : ioTree.listFiles()) { | ||||||
|  |                 name = file.getName().toLowerCase(); | ||||||
|  |                 if (name.startsWith(filename)) existingNames.add(name); | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             for (DocumentFile file : docTree.listFiles()) { | ||||||
|  |                 name = file.getName(); | ||||||
|  |                 if (name == null) continue; | ||||||
|  |                 name = name.toLowerCase(); | ||||||
|  |                 if (name.startsWith(filename)) existingNames.add(name); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         boolean free = true; | ||||||
|  |         String lwFilename = filename.toLowerCase(); | ||||||
|  |         for (String testName : existingNames) { | ||||||
|  |             if (testName.equals(lwFilename)) { | ||||||
|  |                 free = false; | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (free) return createFile(filename, mime); | ||||||
|  |  | ||||||
|  |         String[] sortedNames = existingNames.toArray(new String[0]); | ||||||
|  |         Arrays.sort(sortedNames); | ||||||
|  |  | ||||||
|  |         String newName; | ||||||
|  |         int downloadIndex = 0; | ||||||
|  |         do { | ||||||
|  |             newName = filename + " (" + downloadIndex + ")" + ext; | ||||||
|  |             ++downloadIndex; | ||||||
|  |             if (downloadIndex == 1000) {  // Probably an error on our side | ||||||
|  |                 newName = System.currentTimeMillis() + ext; | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |         } while (Arrays.binarySearch(sortedNames, newName) >= 0); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         return createFile(newName, mime); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public boolean isDirect() { | ||||||
|  |         return docTree == null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public Uri getUri() { | ||||||
|  |         return docTree == null ? Uri.fromFile(ioTree) : docTree.getUri(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public boolean exists() { | ||||||
|  |         return docTree == null ? ioTree.exists() : docTree.exists(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getTag() { | ||||||
|  |         return tag; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void acquirePermissions() throws IOException { | ||||||
|  |         if (docTree == null) return; | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             contentResolver.takePersistableUriPermission(docTree.getUri(), PERMISSION_FLAGS); | ||||||
|  |         } catch (Throwable e) { | ||||||
|  |             throw new IOException(e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void revokePermissions() throws IOException { | ||||||
|  |         if (docTree == null) return; | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             contentResolver.releasePersistableUriPermission(docTree.getUri(), PERMISSION_FLAGS); | ||||||
|  |         } catch (Throwable e) { | ||||||
|  |             throw new IOException(e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public Uri findFile(String filename) { | ||||||
|  |         if (docTree == null) | ||||||
|  |             return Uri.fromFile(new File(ioTree, filename)); | ||||||
|  |  | ||||||
|  |         // findFile() method is very slow | ||||||
|  |         DocumentFile file = docTree.findFile(filename); | ||||||
|  |  | ||||||
|  |         return file == null ? null : file.getUri(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @NonNull | ||||||
|  |     @Override | ||||||
|  |     public String toString() { | ||||||
|  |         return docTree == null ? Uri.fromFile(ioTree).toString() : docTree.getUri().toString(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										301
									
								
								app/src/main/java/us/shandian/giga/io/StoredFileHelper.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										301
									
								
								app/src/main/java/us/shandian/giga/io/StoredFileHelper.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,301 @@ | |||||||
|  | package us.shandian.giga.io; | ||||||
|  |  | ||||||
|  | import android.annotation.TargetApi; | ||||||
|  | import android.content.ContentResolver; | ||||||
|  | import android.content.Context; | ||||||
|  | import android.content.Intent; | ||||||
|  | import android.net.Uri; | ||||||
|  | import android.os.Build; | ||||||
|  | import android.provider.DocumentsContract; | ||||||
|  | import android.support.annotation.NonNull; | ||||||
|  | import android.support.v4.app.Fragment; | ||||||
|  | import android.support.v4.provider.DocumentFile; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.streams.io.SharpStream; | ||||||
|  |  | ||||||
|  | import java.io.File; | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.io.Serializable; | ||||||
|  | import java.net.URI; | ||||||
|  |  | ||||||
|  | public class StoredFileHelper implements Serializable { | ||||||
|  |     private static final long serialVersionUID = 0L; | ||||||
|  |     public static final String DEFAULT_MIME = "application/octet-stream"; | ||||||
|  |  | ||||||
|  |     private transient DocumentFile docFile; | ||||||
|  |     private transient DocumentFile docTree; | ||||||
|  |     private transient File ioFile; | ||||||
|  |     private transient ContentResolver contentResolver; | ||||||
|  |  | ||||||
|  |     protected String source; | ||||||
|  |     String sourceTree; | ||||||
|  |  | ||||||
|  |     protected String tag; | ||||||
|  |  | ||||||
|  |     private String srcName; | ||||||
|  |     private String srcType; | ||||||
|  |  | ||||||
|  |     public StoredFileHelper(String filename, String mime, String tag) { | ||||||
|  |         this.source = null;// this instance will be "invalid" see invalidate()/isInvalid() methods | ||||||
|  |  | ||||||
|  |         this.srcName = filename; | ||||||
|  |         this.srcType = mime == null ? DEFAULT_MIME : mime; | ||||||
|  |  | ||||||
|  |         this.tag = tag; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @TargetApi(Build.VERSION_CODES.LOLLIPOP) | ||||||
|  |     StoredFileHelper(DocumentFile tree, ContentResolver contentResolver, String filename, String mime, String tag) throws IOException { | ||||||
|  |         this.docTree = tree; | ||||||
|  |         this.contentResolver = contentResolver; | ||||||
|  |  | ||||||
|  |         // this is very slow, because SAF does not allow overwrite | ||||||
|  |         DocumentFile res = this.docTree.findFile(filename); | ||||||
|  |  | ||||||
|  |         if (res != null && res.exists() && res.isDirectory()) { | ||||||
|  |             if (!res.delete()) | ||||||
|  |                 throw new IOException("Directory with the same name found but cannot delete"); | ||||||
|  |             res = null; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (res == null) { | ||||||
|  |             res = this.docTree.createFile(mime == null ? DEFAULT_MIME : mime, filename); | ||||||
|  |             if (res == null) throw new IOException("Cannot create the file"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         this.docFile = res; | ||||||
|  |         this.source = res.getUri().toString(); | ||||||
|  |         this.srcName = getName(); | ||||||
|  |         this.srcType = getType(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @TargetApi(Build.VERSION_CODES.KITKAT) | ||||||
|  |     public StoredFileHelper(Context context, @NonNull Uri path, String tag) throws IOException { | ||||||
|  |         this.source = path.toString(); | ||||||
|  |         this.tag = tag; | ||||||
|  |  | ||||||
|  |         if (path.getScheme() == null || path.getScheme().equalsIgnoreCase("file")) { | ||||||
|  |             this.ioFile = new File(URI.create(this.source)); | ||||||
|  |         } else { | ||||||
|  |             DocumentFile file = DocumentFile.fromSingleUri(context, path); | ||||||
|  |             if (file == null) | ||||||
|  |                 throw new UnsupportedOperationException("Cannot get the file via SAF"); | ||||||
|  |  | ||||||
|  |             this.contentResolver = context.getContentResolver(); | ||||||
|  |             this.docFile = file; | ||||||
|  |  | ||||||
|  |             try { | ||||||
|  |                 this.contentResolver.takePersistableUriPermission(docFile.getUri(), StoredDirectoryHelper.PERMISSION_FLAGS); | ||||||
|  |             } catch (Exception e) { | ||||||
|  |                 throw new IOException(e); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         this.srcName = getName(); | ||||||
|  |         this.srcType = getType(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public StoredFileHelper(File location, String filename, String tag) throws IOException { | ||||||
|  |         this.ioFile = new File(location, filename); | ||||||
|  |         this.tag = tag; | ||||||
|  |  | ||||||
|  |         if (this.ioFile.exists()) { | ||||||
|  |             if (!this.ioFile.isFile() && !this.ioFile.delete()) | ||||||
|  |                 throw new IOException("The filename is already in use by non-file entity and cannot overwrite it"); | ||||||
|  |         } else { | ||||||
|  |             if (!this.ioFile.createNewFile()) | ||||||
|  |                 throw new IOException("Cannot create the file"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         this.source = Uri.fromFile(this.ioFile).toString(); | ||||||
|  |         this.srcName = getName(); | ||||||
|  |         this.srcType = getType(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static StoredFileHelper deserialize(@NonNull StoredFileHelper storage, Context context) throws IOException { | ||||||
|  |         if (storage.isInvalid()) | ||||||
|  |             return new StoredFileHelper(storage.srcName, storage.srcType, storage.tag); | ||||||
|  |  | ||||||
|  |         StoredFileHelper instance = new StoredFileHelper(context, Uri.parse(storage.source), storage.tag); | ||||||
|  |  | ||||||
|  |         if (storage.sourceTree != null) { | ||||||
|  |             instance.docTree = DocumentFile.fromTreeUri(context, Uri.parse(instance.sourceTree)); | ||||||
|  |  | ||||||
|  |             if (instance.docTree == null) | ||||||
|  |                 throw new IOException("Cannot deserialize the tree, ¿revoked permissions?"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return instance; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static void requestSafWithFileCreation(@NonNull Fragment who, int requestCode, String filename, String mime) { | ||||||
|  |         // SAF notes: | ||||||
|  |         //           ACTION_OPEN_DOCUMENT       Do not let you create the file, useful for overwrite files | ||||||
|  |         //           ACTION_CREATE_DOCUMENT     No overwrite support, useless the file provider resolve the conflict | ||||||
|  |  | ||||||
|  |         Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT) | ||||||
|  |                 .addCategory(Intent.CATEGORY_OPENABLE) | ||||||
|  |                 .setType(mime) | ||||||
|  |                 .putExtra(Intent.EXTRA_TITLE, filename) | ||||||
|  |                 .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS) | ||||||
|  |                 .putExtra("android.content.extra.SHOW_ADVANCED", true);// hack, show all storage disks | ||||||
|  |  | ||||||
|  |         who.startActivityForResult(intent, requestCode); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public SharpStream getStream() throws IOException { | ||||||
|  |         invalid(); | ||||||
|  |  | ||||||
|  |         if (docFile == null) | ||||||
|  |             return new FileStream(ioFile); | ||||||
|  |         else | ||||||
|  |             return new FileStreamSAF(contentResolver, docFile.getUri()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Indicates whatever if is possible access using the {@code java.io} API | ||||||
|  |      * | ||||||
|  |      * @return {@code true} for Java I/O API, otherwise, {@code false} for Storage Access Framework | ||||||
|  |      */ | ||||||
|  |     public boolean isDirect() { | ||||||
|  |         invalid(); | ||||||
|  |  | ||||||
|  |         return docFile == null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public boolean isInvalid() { | ||||||
|  |         return source == null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public Uri getUri() { | ||||||
|  |         invalid(); | ||||||
|  |  | ||||||
|  |         return docFile == null ? Uri.fromFile(ioFile) : docFile.getUri(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void truncate() throws IOException { | ||||||
|  |         invalid(); | ||||||
|  |  | ||||||
|  |         try (SharpStream fs = getStream()) { | ||||||
|  |             fs.setLength(0); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public boolean delete() { | ||||||
|  |         invalid(); | ||||||
|  |  | ||||||
|  |         if (docFile == null) return ioFile.delete(); | ||||||
|  |  | ||||||
|  |         boolean res = docFile.delete(); | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; | ||||||
|  |             contentResolver.releasePersistableUriPermission(docFile.getUri(), flags); | ||||||
|  |         } catch (Exception ex) { | ||||||
|  |             // ¿what happen? | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return res; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public long length() { | ||||||
|  |         invalid(); | ||||||
|  |  | ||||||
|  |         return docFile == null ? ioFile.length() : docFile.length(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public boolean canWrite() { | ||||||
|  |         if (source == null) return false; | ||||||
|  |         return docFile == null ? ioFile.canWrite() : docFile.canWrite(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public File getIOFile() { | ||||||
|  |         return ioFile; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getName() { | ||||||
|  |         if (source == null) return srcName; | ||||||
|  |         return docFile == null ? ioFile.getName() : docFile.getName(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getType() { | ||||||
|  |         if (source == null) return srcType; | ||||||
|  |         return docFile == null ? DEFAULT_MIME : docFile.getType();// not obligatory for Java IO | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getTag() { | ||||||
|  |         return tag; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public boolean existsAsFile() { | ||||||
|  |         if (source == null) return false; | ||||||
|  |  | ||||||
|  |         boolean exists = docFile == null ? ioFile.exists() : docFile.exists(); | ||||||
|  |         boolean asFile = docFile == null ? ioFile.isFile() : docFile.isFile();// ¿docFile.isVirtual() means is no-physical? | ||||||
|  |  | ||||||
|  |         return exists && asFile; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public boolean create() { | ||||||
|  |         invalid(); | ||||||
|  |  | ||||||
|  |         if (docFile == null) { | ||||||
|  |             try { | ||||||
|  |                 return ioFile.createNewFile(); | ||||||
|  |             } catch (IOException e) { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (docTree == null || docFile.getName() == null) return false; | ||||||
|  |  | ||||||
|  |         DocumentFile res = docTree.createFile(docFile.getName(), docFile.getType() == null ? DEFAULT_MIME : docFile.getType()); | ||||||
|  |         if (res == null) return false; | ||||||
|  |  | ||||||
|  |         docFile = res; | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void invalidate() { | ||||||
|  |         if (source == null) return; | ||||||
|  |  | ||||||
|  |         srcName = getName(); | ||||||
|  |         srcType = getType(); | ||||||
|  |  | ||||||
|  |         source = null; | ||||||
|  |  | ||||||
|  |         sourceTree = null; | ||||||
|  |         docTree = null; | ||||||
|  |         docFile = null; | ||||||
|  |         ioFile = null; | ||||||
|  |         contentResolver = null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void invalid() { | ||||||
|  |         if (source == null) | ||||||
|  |             throw new IllegalStateException("In invalid state"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public boolean equals(StoredFileHelper storage) { | ||||||
|  |         if (this.isInvalid() != storage.isInvalid()) return false; | ||||||
|  |         if (this.isDirect() != storage.isDirect()) return false; | ||||||
|  |  | ||||||
|  |         if (this.isDirect()) | ||||||
|  |             return this.ioFile.getPath().equalsIgnoreCase(storage.ioFile.getPath()); | ||||||
|  |  | ||||||
|  |         return DocumentsContract.getDocumentId( | ||||||
|  |                 this.docFile.getUri() | ||||||
|  |         ).equalsIgnoreCase(DocumentsContract.getDocumentId( | ||||||
|  |                 storage.docFile.getUri() | ||||||
|  |         )); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @NonNull | ||||||
|  |     @Override | ||||||
|  |     public String toString() { | ||||||
|  |         if (source == null) | ||||||
|  |             return "[Invalid state] name=" + srcName + "  type=" + srcType + "  tag=" + tag; | ||||||
|  |         else | ||||||
|  |             return "sourceFile=" + source + "  treeSource=" + (sourceTree == null ? "" : sourceTree) + "  tag=" + tag; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -6,12 +6,10 @@ import org.schabi.newpipe.streams.io.SharpStream; | |||||||
|  |  | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
|  |  | ||||||
| import us.shandian.giga.get.DownloadMission; |  | ||||||
|  |  | ||||||
| public class M4aNoDash extends Postprocessing { | public class M4aNoDash extends Postprocessing { | ||||||
|  |  | ||||||
|     M4aNoDash(DownloadMission mission) { |     M4aNoDash() { | ||||||
|         super(mission, 0, true); |         super(0, true); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|   | |||||||
| @@ -5,15 +5,13 @@ import org.schabi.newpipe.streams.io.SharpStream; | |||||||
|  |  | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
|  |  | ||||||
| import us.shandian.giga.get.DownloadMission; |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @author kapodamy |  * @author kapodamy | ||||||
|  */ |  */ | ||||||
| class Mp4FromDashMuxer extends Postprocessing { | class Mp4FromDashMuxer extends Postprocessing { | ||||||
|  |  | ||||||
|     Mp4FromDashMuxer(DownloadMission mission) { |     Mp4FromDashMuxer() { | ||||||
|         super(mission, 2 * 1024 * 1024/* 2 MiB */, true); |         super(2 * 1024 * 1024/* 2 MiB */, true); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| package us.shandian.giga.postprocessing; | package us.shandian.giga.postprocessing; | ||||||
|  |  | ||||||
| import android.os.Message; | import android.os.Message; | ||||||
|  | import android.support.annotation.NonNull; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.streams.io.SharpStream; | import org.schabi.newpipe.streams.io.SharpStream; | ||||||
| @@ -9,9 +10,9 @@ import java.io.File; | |||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
|  |  | ||||||
| import us.shandian.giga.get.DownloadMission; | import us.shandian.giga.get.DownloadMission; | ||||||
| import us.shandian.giga.postprocessing.io.ChunkFileInputStream; | import us.shandian.giga.io.ChunkFileInputStream; | ||||||
| import us.shandian.giga.postprocessing.io.CircularFileWriter; | import us.shandian.giga.io.CircularFileWriter; | ||||||
| import us.shandian.giga.postprocessing.io.CircularFileWriter.OffsetChecker; | import us.shandian.giga.io.CircularFileWriter.OffsetChecker; | ||||||
| import us.shandian.giga.service.DownloadManagerService; | import us.shandian.giga.service.DownloadManagerService; | ||||||
|  |  | ||||||
| import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING; | import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING; | ||||||
| @@ -20,30 +21,41 @@ import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; | |||||||
|  |  | ||||||
| public abstract class Postprocessing { | public abstract class Postprocessing { | ||||||
|  |  | ||||||
|     static final byte OK_RESULT = ERROR_NOTHING; |     static transient final byte OK_RESULT = ERROR_NOTHING; | ||||||
|  |  | ||||||
|     public static final String ALGORITHM_TTML_CONVERTER = "ttml"; |     public transient static final String ALGORITHM_TTML_CONVERTER = "ttml"; | ||||||
|     public static final String ALGORITHM_WEBM_MUXER = "webm"; |     public transient static final String ALGORITHM_WEBM_MUXER = "webm"; | ||||||
|     public static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4"; |     public transient static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4"; | ||||||
|     public static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a"; |     public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a"; | ||||||
|  |  | ||||||
|  |     public static Postprocessing getAlgorithm(String algorithmName, String[] args) { | ||||||
|  |         Postprocessing instance; | ||||||
|  |  | ||||||
|     public static Postprocessing getAlgorithm(String algorithmName, DownloadMission mission) { |  | ||||||
|         if (null == algorithmName) { |         if (null == algorithmName) { | ||||||
|             throw new NullPointerException("algorithmName"); |             throw new NullPointerException("algorithmName"); | ||||||
|         } else switch (algorithmName) { |         } else switch (algorithmName) { | ||||||
|             case ALGORITHM_TTML_CONVERTER: |             case ALGORITHM_TTML_CONVERTER: | ||||||
|                 return new TtmlConverter(mission); |                 instance = new TtmlConverter(); | ||||||
|  |                 break; | ||||||
|             case ALGORITHM_WEBM_MUXER: |             case ALGORITHM_WEBM_MUXER: | ||||||
|                 return new WebMMuxer(mission); |                 instance = new WebMMuxer(); | ||||||
|  |                 break; | ||||||
|             case ALGORITHM_MP4_FROM_DASH_MUXER: |             case ALGORITHM_MP4_FROM_DASH_MUXER: | ||||||
|                 return new Mp4FromDashMuxer(mission); |                 instance = new Mp4FromDashMuxer(); | ||||||
|  |                 break; | ||||||
|             case ALGORITHM_M4A_NO_DASH: |             case ALGORITHM_M4A_NO_DASH: | ||||||
|                 return new M4aNoDash(mission); |                 instance = new M4aNoDash(); | ||||||
|  |                 break; | ||||||
|             /*case "example-algorithm": |             /*case "example-algorithm": | ||||||
|                 return new ExampleAlgorithm(mission);*/ |                 instance = new ExampleAlgorithm(mission);*/ | ||||||
|             default: |             default: | ||||||
|                 throw new RuntimeException("Unimplemented post-processing algorithm: " + algorithmName); |                 throw new RuntimeException("Unimplemented post-processing algorithm: " + algorithmName); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         instance.args = args; | ||||||
|  |         instance.name = algorithmName; | ||||||
|  |  | ||||||
|  |         return instance; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -61,32 +73,38 @@ public abstract class Postprocessing { | |||||||
|     /** |     /** | ||||||
|      * the download to post-process |      * the download to post-process | ||||||
|      */ |      */ | ||||||
|     protected DownloadMission mission; |     protected transient DownloadMission mission; | ||||||
|  |  | ||||||
|     Postprocessing(DownloadMission mission, int recommendedReserve, boolean worksOnSameFile) { |     public transient File cacheDir; | ||||||
|         this.mission = mission; |  | ||||||
|  |     private String[] args; | ||||||
|  |  | ||||||
|  |     private String name; | ||||||
|  |  | ||||||
|  |     Postprocessing(int recommendedReserve, boolean worksOnSameFile) { | ||||||
|         this.recommendedReserve = recommendedReserve; |         this.recommendedReserve = recommendedReserve; | ||||||
|         this.worksOnSameFile = worksOnSameFile; |         this.worksOnSameFile = worksOnSameFile; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void run() throws IOException { |     public void run(DownloadMission target) throws IOException { | ||||||
|         File file = mission.getDownloadedFile(); |         this.mission = target; | ||||||
|  |  | ||||||
|         File temp = null; |         File temp = null; | ||||||
|         CircularFileWriter out = null; |         CircularFileWriter out = null; | ||||||
|         int result; |         int result; | ||||||
|         long finalLength = -1; |         long finalLength = -1; | ||||||
|  |  | ||||||
|         mission.done = 0; |         mission.done = 0; | ||||||
|         mission.length = file.length(); |         mission.length = mission.storage.length(); | ||||||
|  |  | ||||||
|         if (worksOnSameFile) { |         if (worksOnSameFile) { | ||||||
|             ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length]; |             ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length]; | ||||||
|             try { |             try { | ||||||
|                 int i = 0; |                 int i = 0; | ||||||
|                 for (; i < sources.length - 1; i++) { |                 for (; i < sources.length - 1; i++) { | ||||||
|                     sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.offsets[i + 1], "rw"); |                     sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i], mission.offsets[i + 1]); | ||||||
|                 } |                 } | ||||||
|                 sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.getDownloadedFile().length(), "rw"); |                 sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i]); | ||||||
|  |  | ||||||
|                 if (test(sources)) { |                 if (test(sources)) { | ||||||
|                     for (SharpStream source : sources) source.rewind(); |                     for (SharpStream source : sources) source.rewind(); | ||||||
| @@ -97,7 +115,7 @@ public abstract class Postprocessing { | |||||||
|                              * WARNING: never use rewind() in any chunk after any writing (especially on first chunks) |                              * WARNING: never use rewind() in any chunk after any writing (especially on first chunks) | ||||||
|                              *          or the CircularFileWriter can lead to unexpected results |                              *          or the CircularFileWriter can lead to unexpected results | ||||||
|                              */ |                              */ | ||||||
|                             if (source.isDisposed() || source.available() < 1) { |                             if (source.isClosed() || source.available() < 1) { | ||||||
|                                 continue;// the selected source is not used anymore |                                 continue;// the selected source is not used anymore | ||||||
|                             } |                             } | ||||||
|  |  | ||||||
| @@ -107,18 +125,19 @@ public abstract class Postprocessing { | |||||||
|                         return -1; |                         return -1; | ||||||
|                     }; |                     }; | ||||||
|  |  | ||||||
|                     temp = new File(mission.location, mission.name + ".tmp"); |                     // TODO: use Context.getCache() for this operation | ||||||
|  |                     temp = new File(cacheDir, mission.storage.getName() + ".tmp"); | ||||||
|  |  | ||||||
|                     out = new CircularFileWriter(file, temp, checker); |                     out = new CircularFileWriter(mission.storage.getStream(), temp, checker); | ||||||
|                     out.onProgress = this::progressReport; |                     out.onProgress = this::progressReport; | ||||||
|  |  | ||||||
|                     out.onWriteError = (err) -> { |                     out.onWriteError = (err) -> { | ||||||
|                         mission.postprocessingState = 3; |                         mission.psState = 3; | ||||||
|                         mission.notifyError(ERROR_POSTPROCESSING_HOLD, err); |                         mission.notifyError(ERROR_POSTPROCESSING_HOLD, err); | ||||||
|  |  | ||||||
|                         try { |                         try { | ||||||
|                             synchronized (this) { |                             synchronized (this) { | ||||||
|                                 while (mission.postprocessingState == 3) |                                 while (mission.psState == 3) | ||||||
|                                     wait(); |                                     wait(); | ||||||
|                             } |                             } | ||||||
|                         } catch (InterruptedException e) { |                         } catch (InterruptedException e) { | ||||||
| @@ -138,12 +157,12 @@ public abstract class Postprocessing { | |||||||
|                 } |                 } | ||||||
|             } finally { |             } finally { | ||||||
|                 for (SharpStream source : sources) { |                 for (SharpStream source : sources) { | ||||||
|                     if (source != null && !source.isDisposed()) { |                     if (source != null && !source.isClosed()) { | ||||||
|                         source.dispose(); |                         source.close(); | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                 if (out != null) { |                 if (out != null) { | ||||||
|                     out.dispose(); |                     out.close(); | ||||||
|                 } |                 } | ||||||
|                 if (temp != null) { |                 if (temp != null) { | ||||||
|                     //noinspection ResultOfMethodCallIgnored |                     //noinspection ResultOfMethodCallIgnored | ||||||
| @@ -164,10 +183,9 @@ public abstract class Postprocessing { | |||||||
|             mission.errObject = new RuntimeException("post-processing algorithm returned " + result); |             mission.errObject = new RuntimeException("post-processing algorithm returned " + result); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (result != OK_RESULT && worksOnSameFile) { |         if (result != OK_RESULT && worksOnSameFile) mission.storage.delete(); | ||||||
|             //noinspection ResultOfMethodCallIgnored |  | ||||||
|             file.delete(); |         this.mission = null; | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -192,11 +210,11 @@ public abstract class Postprocessing { | |||||||
|     abstract int process(SharpStream out, SharpStream... sources) throws IOException; |     abstract int process(SharpStream out, SharpStream... sources) throws IOException; | ||||||
|  |  | ||||||
|     String getArgumentAt(int index, String defaultValue) { |     String getArgumentAt(int index, String defaultValue) { | ||||||
|         if (mission.postprocessingArgs == null || index >= mission.postprocessingArgs.length) { |         if (args == null || index >= args.length) { | ||||||
|             return defaultValue; |             return defaultValue; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return mission.postprocessingArgs[index]; |         return args[index]; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void progressReport(long done) { |     private void progressReport(long done) { | ||||||
| @@ -209,4 +227,22 @@ public abstract class Postprocessing { | |||||||
|  |  | ||||||
|         mission.mHandler.sendMessage(m); |         mission.mHandler.sendMessage(m); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @NonNull | ||||||
|  |     @Override | ||||||
|  |     public String toString() { | ||||||
|  |         StringBuilder str = new StringBuilder(); | ||||||
|  |  | ||||||
|  |         str.append("name=").append(name).append('['); | ||||||
|  |  | ||||||
|  |         if (args != null) { | ||||||
|  |             for (String arg : args) { | ||||||
|  |                 str.append(", "); | ||||||
|  |                 str.append(arg); | ||||||
|  |             } | ||||||
|  |             str.delete(0, 1); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return str.append(']').toString(); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,8 +2,8 @@ package us.shandian.giga.postprocessing; | |||||||
|  |  | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.streams.io.SharpStream; |  | ||||||
| import org.schabi.newpipe.streams.SubtitleConverter; | import org.schabi.newpipe.streams.SubtitleConverter; | ||||||
|  | import org.schabi.newpipe.streams.io.SharpStream; | ||||||
| import org.xml.sax.SAXException; | import org.xml.sax.SAXException; | ||||||
|  |  | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| @@ -12,18 +12,15 @@ import java.text.ParseException; | |||||||
| import javax.xml.parsers.ParserConfigurationException; | import javax.xml.parsers.ParserConfigurationException; | ||||||
| import javax.xml.xpath.XPathExpressionException; | import javax.xml.xpath.XPathExpressionException; | ||||||
|  |  | ||||||
| import us.shandian.giga.get.DownloadMission; |  | ||||||
| import us.shandian.giga.postprocessing.io.SharpInputStream; |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @author kapodamy |  * @author kapodamy | ||||||
|  */ |  */ | ||||||
| class TtmlConverter extends Postprocessing { | class TtmlConverter extends Postprocessing { | ||||||
|     private static final String TAG = "TtmlConverter"; |     private static final String TAG = "TtmlConverter"; | ||||||
|  |  | ||||||
|     TtmlConverter(DownloadMission mission) { |     TtmlConverter() { | ||||||
|         // due how XmlPullParser works, the xml is fully loaded on the ram |         // due how XmlPullParser works, the xml is fully loaded on the ram | ||||||
|         super(mission, 0, true); |         super(0, true); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|   | |||||||
| @@ -7,15 +7,13 @@ import org.schabi.newpipe.streams.io.SharpStream; | |||||||
|  |  | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
|  |  | ||||||
| import us.shandian.giga.get.DownloadMission; |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @author kapodamy |  * @author kapodamy | ||||||
|  */ |  */ | ||||||
| class WebMMuxer extends Postprocessing { | class WebMMuxer extends Postprocessing { | ||||||
|  |  | ||||||
|     WebMMuxer(DownloadMission mission) { |     WebMMuxer() { | ||||||
|         super(mission, 2048 * 1024/* 2 MiB */, true); |         super(2048 * 1024/* 2 MiB */, true); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|   | |||||||
| @@ -13,16 +13,15 @@ import org.schabi.newpipe.R; | |||||||
| import java.io.File; | import java.io.File; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.Arrays; |  | ||||||
| import java.util.Collections; | import java.util.Collections; | ||||||
| import java.util.Iterator; | import java.util.Iterator; | ||||||
|  |  | ||||||
| import us.shandian.giga.get.DownloadMission; | import us.shandian.giga.get.DownloadMission; | ||||||
| import us.shandian.giga.get.FinishedMission; | import us.shandian.giga.get.FinishedMission; | ||||||
| import us.shandian.giga.get.Mission; | import us.shandian.giga.get.Mission; | ||||||
| import us.shandian.giga.get.sqlite.DownloadDataSource; | import us.shandian.giga.get.sqlite.FinishedMissionStore; | ||||||
| import us.shandian.giga.service.DownloadManagerService.DMChecker; | import us.shandian.giga.io.StoredDirectoryHelper; | ||||||
| import us.shandian.giga.service.DownloadManagerService.MissionCheck; | import us.shandian.giga.io.StoredFileHelper; | ||||||
| import us.shandian.giga.util.Utility; | import us.shandian.giga.util.Utility; | ||||||
|  |  | ||||||
| import static org.schabi.newpipe.BuildConfig.DEBUG; | import static org.schabi.newpipe.BuildConfig.DEBUG; | ||||||
| @@ -36,7 +35,10 @@ public class DownloadManager { | |||||||
|     public final static int SPECIAL_PENDING = 1; |     public final static int SPECIAL_PENDING = 1; | ||||||
|     public final static int SPECIAL_FINISHED = 2; |     public final static int SPECIAL_FINISHED = 2; | ||||||
|  |  | ||||||
|     private final DownloadDataSource mDownloadDataSource; |     static final String TAG_AUDIO = "audio"; | ||||||
|  |     static final String TAG_VIDEO = "video"; | ||||||
|  |  | ||||||
|  |     private final FinishedMissionStore mFinishedMissionStore; | ||||||
|  |  | ||||||
|     private final ArrayList<DownloadMission> mMissionsPending = new ArrayList<>(); |     private final ArrayList<DownloadMission> mMissionsPending = new ArrayList<>(); | ||||||
|     private final ArrayList<FinishedMission> mMissionsFinished; |     private final ArrayList<FinishedMission> mMissionsFinished; | ||||||
| @@ -51,6 +53,9 @@ public class DownloadManager { | |||||||
|     boolean mPrefQueueLimit; |     boolean mPrefQueueLimit; | ||||||
|     private boolean mSelfMissionsControl; |     private boolean mSelfMissionsControl; | ||||||
|  |  | ||||||
|  |     StoredDirectoryHelper mMainStorageAudio; | ||||||
|  |     StoredDirectoryHelper mMainStorageVideo; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Create a new instance |      * Create a new instance | ||||||
|      * |      * | ||||||
| @@ -62,7 +67,7 @@ public class DownloadManager { | |||||||
|             Log.d(TAG, "new DownloadManager instance. 0x" + Integer.toHexString(this.hashCode())); |             Log.d(TAG, "new DownloadManager instance. 0x" + Integer.toHexString(this.hashCode())); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         mDownloadDataSource = new DownloadDataSource(context); |         mFinishedMissionStore = new FinishedMissionStore(context); | ||||||
|         mHandler = handler; |         mHandler = handler; | ||||||
|         mMissionsFinished = loadFinishedMissions(); |         mMissionsFinished = loadFinishedMissions(); | ||||||
|         mPendingMissionsDir = getPendingDir(context); |         mPendingMissionsDir = getPendingDir(context); | ||||||
| @@ -71,7 +76,7 @@ public class DownloadManager { | |||||||
|             throw new RuntimeException("failed to create pending_downloads in data directory"); |             throw new RuntimeException("failed to create pending_downloads in data directory"); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         loadPendingMissions(); |         loadPendingMissions(context); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private static File getPendingDir(@NonNull Context context) { |     private static File getPendingDir(@NonNull Context context) { | ||||||
| @@ -92,29 +97,24 @@ public class DownloadManager { | |||||||
|      * Loads finished missions from the data source |      * Loads finished missions from the data source | ||||||
|      */ |      */ | ||||||
|     private ArrayList<FinishedMission> loadFinishedMissions() { |     private ArrayList<FinishedMission> loadFinishedMissions() { | ||||||
|         ArrayList<FinishedMission> finishedMissions = mDownloadDataSource.loadFinishedMissions(); |         ArrayList<FinishedMission> finishedMissions = mFinishedMissionStore.loadFinishedMissions(); | ||||||
|  |  | ||||||
|         // missions always is stored by creation order, simply reverse the list |         // check if the files exists, otherwise, forget the download | ||||||
|         ArrayList<FinishedMission> result = new ArrayList<>(finishedMissions.size()); |  | ||||||
|         for (int i = finishedMissions.size() - 1; i >= 0; i--) { |         for (int i = finishedMissions.size() - 1; i >= 0; i--) { | ||||||
|             FinishedMission mission = finishedMissions.get(i); |             FinishedMission mission = finishedMissions.get(i); | ||||||
|             File file = mission.getDownloadedFile(); |  | ||||||
|  |  | ||||||
|             if (!file.isFile()) { |             if (!mission.storage.existsAsFile()) { | ||||||
|                 if (DEBUG) { |                 if (DEBUG) Log.d(TAG, "downloaded file removed: " + mission.storage.getName()); | ||||||
|                     Log.d(TAG, "downloaded file removed: " + file.getAbsolutePath()); |  | ||||||
|  |                 mFinishedMissionStore.deleteMission(mission); | ||||||
|  |                 finishedMissions.remove(i); | ||||||
|             } |             } | ||||||
|                 mDownloadDataSource.deleteMission(mission); |  | ||||||
|                 continue; |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|             result.add(mission); |         return finishedMissions; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|         return result; |     private void loadPendingMissions(Context ctx) { | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void loadPendingMissions() { |  | ||||||
|         File[] subs = mPendingMissionsDir.listFiles(); |         File[] subs = mPendingMissionsDir.listFiles(); | ||||||
|  |  | ||||||
|         if (subs == null) { |         if (subs == null) { | ||||||
| @@ -142,40 +142,63 @@ public class DownloadManager { | |||||||
|                         continue; |                         continue; | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|                     File dl = mis.getDownloadedFile(); |                     boolean exists; | ||||||
|                     boolean exists = dl.exists(); |                     try { | ||||||
|  |                         mis.storage = StoredFileHelper.deserialize(mis.storage, ctx); | ||||||
|  |                         exists = !mis.storage.isInvalid() && mis.storage.existsAsFile(); | ||||||
|  |  | ||||||
|  |                     } catch (Exception ex) { | ||||||
|  |                         Log.e(TAG, "Failed to load the file source of " + mis.storage.toString()); | ||||||
|  |                         mis.storage.invalidate(); | ||||||
|  |                         exists = false; | ||||||
|  |                     } | ||||||
|  |  | ||||||
|                     if (mis.isPsRunning()) { |                     if (mis.isPsRunning()) { | ||||||
|                         if (mis.postprocessingThis) { |                         if (mis.psAlgorithm.worksOnSameFile) { | ||||||
|                             // Incomplete post-processing results in a corrupted download file |                             // Incomplete post-processing results in a corrupted download file | ||||||
|                             // because the selected algorithm works on the same file to save space. |                             // because the selected algorithm works on the same file to save space. | ||||||
|                             if (exists && dl.isFile() && !dl.delete()) |                             if (exists && !mis.storage.delete()) | ||||||
|                                 Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath()); |                                 Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath()); | ||||||
|  |  | ||||||
|                             exists = true; |                             exists = true; | ||||||
|                         } |                         } | ||||||
|  |  | ||||||
|                         mis.postprocessingState = 0; |                         mis.psState = 0; | ||||||
|                         mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED; |                         mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED; | ||||||
|                         mis.errObject = null; |                         mis.errObject = null; | ||||||
|                     } else if (exists && !dl.isFile()) { |                     } else if (!exists) { | ||||||
|                         // probably a folder, this should never happens |  | ||||||
|                         if (!sub.delete()) { |                         StoredDirectoryHelper mainStorage = getMainStorage(mis.storage.getTag()); | ||||||
|                             Log.w(TAG, "Unable to delete serialized file: " + sub.getPath()); |  | ||||||
|                         } |                         if (!mis.storage.isInvalid() && !mis.storage.create()) { | ||||||
|                         continue; |                             // using javaIO cannot recreate the file | ||||||
|  |                             // using SAF in older devices (no tree available) | ||||||
|  |                             // | ||||||
|  |                             // force the user to pick again the save path | ||||||
|  |                             mis.storage.invalidate(); | ||||||
|  |                         } else if (mainStorage != null) { | ||||||
|  |                             // if the user has changed the save path before this download, the original save path will be lost | ||||||
|  |                             StoredFileHelper newStorage = mainStorage.createFile(mis.storage.getName(), mis.storage.getType()); | ||||||
|  |                             if (newStorage == null) | ||||||
|  |                                 mis.storage.invalidate(); | ||||||
|  |                             else | ||||||
|  |                                 mis.storage = newStorage; | ||||||
|                         } |                         } | ||||||
|  |  | ||||||
|                     if (!exists && mis.isInitialized()) { |                         if (mis.isInitialized()) { | ||||||
|                         // downloaded file deleted, reset mission state |                             // the progress is lost, reset mission state | ||||||
|                         DownloadMission m = new DownloadMission(mis.urls, mis.name, mis.location, mis.kind, mis.postprocessingName, mis.postprocessingArgs); |                             DownloadMission m = new DownloadMission(mis.urls, mis.storage, mis.kind, mis.psAlgorithm); | ||||||
|                             m.timestamp = mis.timestamp; |                             m.timestamp = mis.timestamp; | ||||||
|                             m.threadCount = mis.threadCount; |                             m.threadCount = mis.threadCount; | ||||||
|                             m.source = mis.source; |                             m.source = mis.source; | ||||||
|                             m.nearLength = mis.nearLength; |                             m.nearLength = mis.nearLength; | ||||||
|                         m.setEnqueued(mis.enqueued); |                             m.enqueued = mis.enqueued; | ||||||
|  |                             m.errCode = DownloadMission.ERROR_PROGRESS_LOST; | ||||||
|                             mis = m; |                             mis = m; | ||||||
|                         } |                         } | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     if (mis.psAlgorithm != null) mis.psAlgorithm.cacheDir = ctx.getCacheDir(); | ||||||
|  |  | ||||||
|                     mis.running = false; |                     mis.running = false; | ||||||
|                     mis.recovered = exists; |                     mis.recovered = exists; | ||||||
| @@ -196,51 +219,15 @@ public class DownloadManager { | |||||||
|     /** |     /** | ||||||
|      * Start a new download mission |      * Start a new download mission | ||||||
|      * |      * | ||||||
|      * @param urls     the list of urls to download |      * @param mission the new download mission to add and run (if possible) | ||||||
|      * @param location the location |  | ||||||
|      * @param name     the name of the file to create |  | ||||||
|      * @param kind     type of file (a: audio  v: video  s: subtitle ?: file-extension defined) |  | ||||||
|      * @param threads  the number of threads maximal used to download chunks of the file. |  | ||||||
|      * @param psName   the name of the required post-processing algorithm, or {@code null} to ignore. |  | ||||||
|      * @param source   source url of the resource |  | ||||||
|      * @param psArgs   the arguments for the post-processing algorithm. |  | ||||||
|      */ |      */ | ||||||
|     void startMission(String[] urls, String location, String name, char kind, int threads, |     void startMission(DownloadMission mission) { | ||||||
|                       String source, String psName, String[] psArgs, long nearLength) { |  | ||||||
|         synchronized (this) { |         synchronized (this) { | ||||||
|             // check for existing pending download |  | ||||||
|             DownloadMission pendingMission = getPendingMission(location, name); |  | ||||||
|  |  | ||||||
|             if (pendingMission != null) { |  | ||||||
|                 if (pendingMission.running) { |  | ||||||
|                     // generate unique filename (?) |  | ||||||
|                     try { |  | ||||||
|                         name = generateUniqueName(location, name); |  | ||||||
|                     } catch (Exception e) { |  | ||||||
|                         Log.e(TAG, "Unable to generate unique name", e); |  | ||||||
|                         name = System.currentTimeMillis() + name; |  | ||||||
|                         Log.i(TAG, "Using " + name); |  | ||||||
|                     } |  | ||||||
|                 } else { |  | ||||||
|                     // dispose the mission |  | ||||||
|                     mMissionsPending.remove(pendingMission); |  | ||||||
|                     mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED); |  | ||||||
|                     pendingMission.delete(); |  | ||||||
|                 } |  | ||||||
|             } else { |  | ||||||
|                 // check for existing finished download and dispose (if exists) |  | ||||||
|                 int index = getFinishedMissionIndex(location, name); |  | ||||||
|                 if (index >= 0) mDownloadDataSource.deleteMission(mMissionsFinished.remove(index)); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             DownloadMission mission = new DownloadMission(urls, name, location, kind, psName, psArgs); |  | ||||||
|             mission.timestamp = System.currentTimeMillis(); |             mission.timestamp = System.currentTimeMillis(); | ||||||
|             mission.threadCount = threads; |  | ||||||
|             mission.source = source; |  | ||||||
|             mission.mHandler = mHandler; |             mission.mHandler = mHandler; | ||||||
|             mission.maxRetry = mPrefMaxRetry; |             mission.maxRetry = mPrefMaxRetry; | ||||||
|             mission.nearLength = nearLength; |  | ||||||
|  |  | ||||||
|  |             // create metadata file | ||||||
|             while (true) { |             while (true) { | ||||||
|                 mission.metadata = new File(mPendingMissionsDir, String.valueOf(mission.timestamp)); |                 mission.metadata = new File(mPendingMissionsDir, String.valueOf(mission.timestamp)); | ||||||
|                 if (!mission.metadata.isFile() && !mission.metadata.exists()) { |                 if (!mission.metadata.isFile() && !mission.metadata.exists()) { | ||||||
| @@ -261,6 +248,14 @@ public class DownloadManager { | |||||||
|             // Before continue, save the metadata in case the internet connection is not available |             // Before continue, save the metadata in case the internet connection is not available | ||||||
|             Utility.writeToFile(mission.metadata, mission); |             Utility.writeToFile(mission.metadata, mission); | ||||||
|  |  | ||||||
|  |             if (mission.storage == null) { | ||||||
|  |                 // noting to do here | ||||||
|  |                 mission.errCode = DownloadMission.ERROR_FILE_CREATION; | ||||||
|  |                 if (mission.errObject != null) | ||||||
|  |                     mission.errObject = new IOException("DownloadMission.storage == NULL"); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|             boolean start = !mPrefQueueLimit || getRunningMissionsCount() < 1; |             boolean start = !mPrefQueueLimit || getRunningMissionsCount() < 1; | ||||||
|  |  | ||||||
|             if (canDownloadInCurrentNetwork() && start) { |             if (canDownloadInCurrentNetwork() && start) { | ||||||
| @@ -292,7 +287,7 @@ public class DownloadManager { | |||||||
|                 mMissionsPending.remove(mission); |                 mMissionsPending.remove(mission); | ||||||
|             } else if (mission instanceof FinishedMission) { |             } else if (mission instanceof FinishedMission) { | ||||||
|                 mMissionsFinished.remove(mission); |                 mMissionsFinished.remove(mission); | ||||||
|                 mDownloadDataSource.deleteMission(mission); |                 mFinishedMissionStore.deleteMission(mission); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED); |             mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED); | ||||||
| @@ -300,18 +295,35 @@ public class DownloadManager { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public void forgetMission(StoredFileHelper storage) { | ||||||
|  |         synchronized (this) { | ||||||
|  |             Mission mission = getAnyMission(storage); | ||||||
|  |             if (mission == null) return; | ||||||
|  |  | ||||||
|  |             if (mission instanceof DownloadMission) { | ||||||
|  |                 mMissionsPending.remove(mission); | ||||||
|  |             } else if (mission instanceof FinishedMission) { | ||||||
|  |                 mMissionsFinished.remove(mission); | ||||||
|  |                 mFinishedMissionStore.deleteMission(mission); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED); | ||||||
|  |             mission.storage = null; | ||||||
|  |             mission.delete(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Get a pending mission by its location and name |      * Get a pending mission by its path | ||||||
|      * |      * | ||||||
|      * @param location the location |      * @param storage where the file possible is stored | ||||||
|      * @param name     the name |  | ||||||
|      * @return the mission or null if no such mission exists |      * @return the mission or null if no such mission exists | ||||||
|      */ |      */ | ||||||
|     @Nullable |     @Nullable | ||||||
|     private DownloadMission getPendingMission(String location, String name) { |     private DownloadMission getPendingMission(StoredFileHelper storage) { | ||||||
|         for (DownloadMission mission : mMissionsPending) { |         for (DownloadMission mission : mMissionsPending) { | ||||||
|             if (location.equalsIgnoreCase(mission.location) && name.equalsIgnoreCase(mission.name)) { |             if (mission.storage.equals(storage)) { | ||||||
|                 return mission; |                 return mission; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @@ -319,16 +331,14 @@ public class DownloadManager { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Get a finished mission by its location and name |      * Get a finished mission by its path | ||||||
|      * |      * | ||||||
|      * @param location the location |      * @param storage where the file possible is stored | ||||||
|      * @param name     the name |  | ||||||
|      * @return the mission index or -1 if no such mission exists |      * @return the mission index or -1 if no such mission exists | ||||||
|      */ |      */ | ||||||
|     private int getFinishedMissionIndex(String location, String name) { |     private int getFinishedMissionIndex(StoredFileHelper storage) { | ||||||
|         for (int i = 0; i < mMissionsFinished.size(); i++) { |         for (int i = 0; i < mMissionsFinished.size(); i++) { | ||||||
|             FinishedMission mission = mMissionsFinished.get(i); |             if (mMissionsFinished.get(i).storage.equals(storage)) { | ||||||
|             if (location.equalsIgnoreCase(mission.location) && name.equalsIgnoreCase(mission.name)) { |  | ||||||
|                 return i; |                 return i; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @@ -336,12 +346,12 @@ public class DownloadManager { | |||||||
|         return -1; |         return -1; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public Mission getAnyMission(String location, String name) { |     private Mission getAnyMission(StoredFileHelper storage) { | ||||||
|         synchronized (this) { |         synchronized (this) { | ||||||
|             Mission mission = getPendingMission(location, name); |             Mission mission = getPendingMission(storage); | ||||||
|             if (mission != null) return mission; |             if (mission != null) return mission; | ||||||
|  |  | ||||||
|             int idx = getFinishedMissionIndex(location, name); |             int idx = getFinishedMissionIndex(storage); | ||||||
|             if (idx >= 0) return mMissionsFinished.get(idx); |             if (idx >= 0) return mMissionsFinished.get(idx); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -382,7 +392,7 @@ public class DownloadManager { | |||||||
|  |  | ||||||
|         synchronized (this) { |         synchronized (this) { | ||||||
|             for (DownloadMission mission : mMissionsPending) { |             for (DownloadMission mission : mMissionsPending) { | ||||||
|                 if (mission.running || mission.isPsFailed() || mission.isFinished()) continue; |                 if (mission.running || !mission.canDownload()) continue; | ||||||
|  |  | ||||||
|                 flag = true; |                 flag = true; | ||||||
|                 mission.start(); |                 mission.start(); | ||||||
| @@ -392,58 +402,6 @@ public class DownloadManager { | |||||||
|         if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS); |         if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Splits the filename into name and extension |  | ||||||
|      * <p> |  | ||||||
|      * Dots are ignored if they appear: not at all, at the beginning of the file, |  | ||||||
|      * at the end of the file |  | ||||||
|      * |  | ||||||
|      * @param name the name to split |  | ||||||
|      * @return a string array with a length of 2 containing the name and the extension |  | ||||||
|      */ |  | ||||||
|     private static String[] splitName(String name) { |  | ||||||
|         int dotIndex = name.lastIndexOf('.'); |  | ||||||
|         if (dotIndex <= 0 || (dotIndex == name.length() - 1)) { |  | ||||||
|             return new String[]{name, ""}; |  | ||||||
|         } else { |  | ||||||
|             return new String[]{name.substring(0, dotIndex), name.substring(dotIndex + 1)}; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Generates a unique file name. |  | ||||||
|      * <p> |  | ||||||
|      * e.g. "myName (1).txt" if the name "myName.txt" exists. |  | ||||||
|      * |  | ||||||
|      * @param location the location (to check for existing files) |  | ||||||
|      * @param name     the name of the file |  | ||||||
|      * @return the unique file name |  | ||||||
|      * @throws IllegalArgumentException if the location is not a directory |  | ||||||
|      * @throws SecurityException        if the location is not readable |  | ||||||
|      */ |  | ||||||
|     private static String generateUniqueName(String location, String name) { |  | ||||||
|         if (location == null) throw new NullPointerException("location is null"); |  | ||||||
|         if (name == null) throw new NullPointerException("name is null"); |  | ||||||
|         File destination = new File(location); |  | ||||||
|         if (!destination.isDirectory()) { |  | ||||||
|             throw new IllegalArgumentException("location is not a directory: " + location); |  | ||||||
|         } |  | ||||||
|         final String[] nameParts = splitName(name); |  | ||||||
|         String[] existingName = destination.list((dir, name1) -> name1.startsWith(nameParts[0])); |  | ||||||
|         Arrays.sort(existingName); |  | ||||||
|         String newName; |  | ||||||
|         int downloadIndex = 0; |  | ||||||
|         do { |  | ||||||
|             newName = nameParts[0] + " (" + downloadIndex + ")." + nameParts[1]; |  | ||||||
|             ++downloadIndex; |  | ||||||
|             if (downloadIndex == 1000) {  // Probably an error on our side |  | ||||||
|                 throw new RuntimeException("Too many existing files"); |  | ||||||
|             } |  | ||||||
|         } while (Arrays.binarySearch(existingName, newName) >= 0); |  | ||||||
|         return newName; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Set a pending download as finished |      * Set a pending download as finished | ||||||
|      * |      * | ||||||
| @@ -453,7 +411,7 @@ public class DownloadManager { | |||||||
|         synchronized (this) { |         synchronized (this) { | ||||||
|             mMissionsPending.remove(mission); |             mMissionsPending.remove(mission); | ||||||
|             mMissionsFinished.add(0, new FinishedMission(mission)); |             mMissionsFinished.add(0, new FinishedMission(mission)); | ||||||
|             mDownloadDataSource.addMission(mission); |             mFinishedMissionStore.addFinishedMission(mission); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -474,7 +432,8 @@ public class DownloadManager { | |||||||
|  |  | ||||||
|             boolean flag = false; |             boolean flag = false; | ||||||
|             for (DownloadMission mission : mMissionsPending) { |             for (DownloadMission mission : mMissionsPending) { | ||||||
|                 if (mission.running || !mission.enqueued || mission.isFinished()) continue; |                 if (mission.running || !mission.enqueued || mission.isFinished() || mission.hasInvalidStorage()) | ||||||
|  |                     continue; | ||||||
|  |  | ||||||
|                 resumeMission(mission); |                 resumeMission(mission); | ||||||
|                 if (mPrefQueueLimit) return true; |                 if (mPrefQueueLimit) return true; | ||||||
| @@ -496,7 +455,7 @@ public class DownloadManager { | |||||||
|     public void forgetFinishedDownloads() { |     public void forgetFinishedDownloads() { | ||||||
|         synchronized (this) { |         synchronized (this) { | ||||||
|             for (FinishedMission mission : mMissionsFinished) { |             for (FinishedMission mission : mMissionsFinished) { | ||||||
|                 mDownloadDataSource.deleteMission(mission); |                 mFinishedMissionStore.deleteMission(mission); | ||||||
|             } |             } | ||||||
|             mMissionsFinished.clear(); |             mMissionsFinished.clear(); | ||||||
|         } |         } | ||||||
| @@ -523,7 +482,7 @@ public class DownloadManager { | |||||||
|         int paused = 0; |         int paused = 0; | ||||||
|         synchronized (this) { |         synchronized (this) { | ||||||
|             for (DownloadMission mission : mMissionsPending) { |             for (DownloadMission mission : mMissionsPending) { | ||||||
|                 if (mission.isFinished() || mission.isPsRunning()) continue; |                 if (!mission.canDownload() || mission.isPsRunning()) continue; | ||||||
|  |  | ||||||
|                 if (mission.running && isMetered) { |                 if (mission.running && isMetered) { | ||||||
|                     paused++; |                     paused++; | ||||||
| @@ -565,24 +524,32 @@ public class DownloadManager { | |||||||
|         ), Toast.LENGTH_LONG).show(); |         ), Toast.LENGTH_LONG).show(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     void checkForRunningMission(String location, String name, DMChecker check) { |     public MissionState checkForExistingMission(StoredFileHelper storage) { | ||||||
|         MissionCheck result = MissionCheck.None; |  | ||||||
|  |  | ||||||
|         synchronized (this) { |         synchronized (this) { | ||||||
|             DownloadMission pending = getPendingMission(location, name); |             DownloadMission pending = getPendingMission(storage); | ||||||
|  |  | ||||||
|             if (pending == null) { |             if (pending == null) { | ||||||
|                 if (getFinishedMissionIndex(location, name) >= 0) result = MissionCheck.Finished; |                 if (getFinishedMissionIndex(storage) >= 0) return MissionState.Finished; | ||||||
|             } else { |             } else { | ||||||
|                 if (pending.isFinished()) { |                 if (pending.isFinished()) { | ||||||
|                     result = MissionCheck.Finished;// this never should happen (race-condition) |                     return MissionState.Finished;// this never should happen (race-condition) | ||||||
|                 } else { |                 } else { | ||||||
|                     result = pending.running ? MissionCheck.PendingRunning : MissionCheck.Pending; |                     return pending.running ? MissionState.PendingRunning : MissionState.Pending; | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         check.callback(result); |         return MissionState.None; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Nullable | ||||||
|  |     private StoredDirectoryHelper getMainStorage(@NonNull String tag) { | ||||||
|  |         if (tag.equals(TAG_AUDIO)) return mMainStorageAudio; | ||||||
|  |         if (tag.equals(TAG_VIDEO)) return mMainStorageVideo; | ||||||
|  |  | ||||||
|  |         Log.w(TAG, "Unknown download category, not [audio video]: " + String.valueOf(tag)); | ||||||
|  |  | ||||||
|  |         return null;// this never should happen | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public class MissionIterator extends DiffUtil.Callback { |     public class MissionIterator extends DiffUtil.Callback { | ||||||
| @@ -689,7 +656,7 @@ public class DownloadManager { | |||||||
|  |  | ||||||
|             synchronized (DownloadManager.this) { |             synchronized (DownloadManager.this) { | ||||||
|                 for (DownloadMission mission : mMissionsPending) { |                 for (DownloadMission mission : mMissionsPending) { | ||||||
|                     if (hidden.contains(mission) || mission.isPsFailed() || mission.isFinished()) |                     if (hidden.contains(mission) || mission.canDownload()) | ||||||
|                         continue; |                         continue; | ||||||
|  |  | ||||||
|                     if (mission.running) |                     if (mission.running) | ||||||
| @@ -720,7 +687,14 @@ public class DownloadManager { | |||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
|         public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { |         public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { | ||||||
|             return areItemsTheSame(oldItemPosition, newItemPosition); |             Object x = snapshot.get(oldItemPosition); | ||||||
|  |             Object y = current.get(newItemPosition); | ||||||
|  |  | ||||||
|  |             if (x instanceof Mission && y instanceof Mission) { | ||||||
|  |                 return ((Mission) x).storage.equals(((Mission) y).storage); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return false; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,11 +6,9 @@ import android.app.NotificationManager; | |||||||
| import android.app.PendingIntent; | import android.app.PendingIntent; | ||||||
| import android.app.Service; | import android.app.Service; | ||||||
| import android.content.BroadcastReceiver; | import android.content.BroadcastReceiver; | ||||||
| import android.content.ComponentName; |  | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.content.Intent; | import android.content.Intent; | ||||||
| import android.content.IntentFilter; | import android.content.IntentFilter; | ||||||
| import android.content.ServiceConnection; |  | ||||||
| import android.content.SharedPreferences; | import android.content.SharedPreferences; | ||||||
| import android.graphics.Bitmap; | import android.graphics.Bitmap; | ||||||
| import android.graphics.BitmapFactory; | import android.graphics.BitmapFactory; | ||||||
| @@ -21,12 +19,14 @@ import android.net.NetworkRequest; | |||||||
| import android.net.Uri; | import android.net.Uri; | ||||||
| import android.os.Binder; | import android.os.Binder; | ||||||
| import android.os.Build; | import android.os.Build; | ||||||
|  | import android.os.Environment; | ||||||
| import android.os.Handler; | import android.os.Handler; | ||||||
| import android.os.IBinder; | import android.os.IBinder; | ||||||
| import android.os.Looper; | import android.os.Looper; | ||||||
| import android.os.Message; | import android.os.Message; | ||||||
| import android.preference.PreferenceManager; | import android.preference.PreferenceManager; | ||||||
| import android.support.annotation.NonNull; | import android.support.annotation.NonNull; | ||||||
|  | import android.support.annotation.Nullable; | ||||||
| import android.support.v4.app.NotificationCompat; | import android.support.v4.app.NotificationCompat; | ||||||
| import android.support.v4.app.NotificationCompat.Builder; | import android.support.v4.app.NotificationCompat.Builder; | ||||||
| import android.support.v4.content.PermissionChecker; | import android.support.v4.content.PermissionChecker; | ||||||
| @@ -39,9 +39,13 @@ import org.schabi.newpipe.download.DownloadActivity; | |||||||
| import org.schabi.newpipe.player.helper.LockManager; | import org.schabi.newpipe.player.helper.LockManager; | ||||||
|  |  | ||||||
| import java.io.File; | import java.io.File; | ||||||
|  | import java.io.IOException; | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
|  |  | ||||||
| import us.shandian.giga.get.DownloadMission; | import us.shandian.giga.get.DownloadMission; | ||||||
|  | import us.shandian.giga.io.StoredDirectoryHelper; | ||||||
|  | import us.shandian.giga.io.StoredFileHelper; | ||||||
|  | import us.shandian.giga.postprocessing.Postprocessing; | ||||||
| import us.shandian.giga.service.DownloadManager.NetworkState; | import us.shandian.giga.service.DownloadManager.NetworkState; | ||||||
|  |  | ||||||
| import static org.schabi.newpipe.BuildConfig.APPLICATION_ID; | import static org.schabi.newpipe.BuildConfig.APPLICATION_ID; | ||||||
| @@ -61,19 +65,19 @@ public class DownloadManagerService extends Service { | |||||||
|     private static final int DOWNLOADS_NOTIFICATION_ID = 1001; |     private static final int DOWNLOADS_NOTIFICATION_ID = 1001; | ||||||
|  |  | ||||||
|     private static final String EXTRA_URLS = "DownloadManagerService.extra.urls"; |     private static final String EXTRA_URLS = "DownloadManagerService.extra.urls"; | ||||||
|     private static final String EXTRA_NAME = "DownloadManagerService.extra.name"; |     private static final String EXTRA_PATH = "DownloadManagerService.extra.path"; | ||||||
|     private static final String EXTRA_LOCATION = "DownloadManagerService.extra.location"; |  | ||||||
|     private static final String EXTRA_KIND = "DownloadManagerService.extra.kind"; |     private static final String EXTRA_KIND = "DownloadManagerService.extra.kind"; | ||||||
|     private static final String EXTRA_THREADS = "DownloadManagerService.extra.threads"; |     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_NAME = "DownloadManagerService.extra.postprocessingName"; | ||||||
|     private static final String EXTRA_POSTPROCESSING_ARGS = "DownloadManagerService.extra.postprocessingArgs"; |     private static final String EXTRA_POSTPROCESSING_ARGS = "DownloadManagerService.extra.postprocessingArgs"; | ||||||
|     private static final String EXTRA_SOURCE = "DownloadManagerService.extra.source"; |     private static final String EXTRA_SOURCE = "DownloadManagerService.extra.source"; | ||||||
|     private static final String EXTRA_NEAR_LENGTH = "DownloadManagerService.extra.nearLength"; |     private static final String EXTRA_NEAR_LENGTH = "DownloadManagerService.extra.nearLength"; | ||||||
|  |     private static final String EXTRA_MAIN_STORAGE_TAG = "DownloadManagerService.extra.tag"; | ||||||
|  |  | ||||||
|     private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished"; |     private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished"; | ||||||
|     private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished"; |     private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished"; | ||||||
|  |  | ||||||
|     private DMBinder mBinder; |     private DownloadManagerBinder mBinder; | ||||||
|     private DownloadManager mManager; |     private DownloadManager mManager; | ||||||
|     private Notification mNotification; |     private Notification mNotification; | ||||||
|     private Handler mHandler; |     private Handler mHandler; | ||||||
| @@ -110,10 +114,10 @@ public class DownloadManagerService extends Service { | |||||||
|     /** |     /** | ||||||
|      * notify media scanner on downloaded media file ... |      * notify media scanner on downloaded media file ... | ||||||
|      * |      * | ||||||
|      * @param file the downloaded file |      * @param file the downloaded file uri | ||||||
|      */ |      */ | ||||||
|     private void notifyMediaScanner(File file) { |     private void notifyMediaScanner(Uri file) { | ||||||
|         sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file))); |         sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, file)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -124,7 +128,7 @@ public class DownloadManagerService extends Service { | |||||||
|             Log.d(TAG, "onCreate"); |             Log.d(TAG, "onCreate"); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         mBinder = new DMBinder(); |         mBinder = new DownloadManagerBinder(); | ||||||
|         mHandler = new Handler(Looper.myLooper()) { |         mHandler = new Handler(Looper.myLooper()) { | ||||||
|             @Override |             @Override | ||||||
|             public void handleMessage(Message msg) { |             public void handleMessage(Message msg) { | ||||||
| @@ -186,10 +190,12 @@ public class DownloadManagerService extends Service { | |||||||
|         handlePreferenceChange(mPrefs, getString(R.string.downloads_queue_limit)); |         handlePreferenceChange(mPrefs, getString(R.string.downloads_queue_limit)); | ||||||
|  |  | ||||||
|         mLock = new LockManager(this); |         mLock = new LockManager(this); | ||||||
|  |  | ||||||
|  |         setupStorageAPI(true); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public int onStartCommand(Intent intent, int flags, int startId) { |     public int onStartCommand(final Intent intent, int flags, int startId) { | ||||||
|         if (DEBUG) { |         if (DEBUG) { | ||||||
|             Log.d(TAG, intent == null ? "Restarting" : "Starting"); |             Log.d(TAG, intent == null ? "Restarting" : "Starting"); | ||||||
|         } |         } | ||||||
| @@ -200,20 +206,7 @@ public class DownloadManagerService extends Service { | |||||||
|         String action = intent.getAction(); |         String action = intent.getAction(); | ||||||
|         if (action != null) { |         if (action != null) { | ||||||
|             if (action.equals(Intent.ACTION_RUN)) { |             if (action.equals(Intent.ACTION_RUN)) { | ||||||
|                 String[] urls = intent.getStringArrayExtra(EXTRA_URLS); |                 mHandler.post(() -> startMission(intent)); | ||||||
|                 String name = intent.getStringExtra(EXTRA_NAME); |  | ||||||
|                 String location = intent.getStringExtra(EXTRA_LOCATION); |  | ||||||
|                 int threads = intent.getIntExtra(EXTRA_THREADS, 1); |  | ||||||
|                 char kind = intent.getCharExtra(EXTRA_KIND, '?'); |  | ||||||
|                 String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME); |  | ||||||
|                 String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS); |  | ||||||
|                 String source = intent.getStringExtra(EXTRA_SOURCE); |  | ||||||
|                 long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0); |  | ||||||
|  |  | ||||||
|                 handleConnectivityState(true);// first check the actual network status |  | ||||||
|  |  | ||||||
|                 mHandler.post(() -> mManager.startMission(urls, location, name, kind, threads, source, psName, psArgs, nearLength)); |  | ||||||
|  |  | ||||||
|             } else if (downloadDoneNotification != null) { |             } else if (downloadDoneNotification != null) { | ||||||
|                 if (action.equals(ACTION_RESET_DOWNLOAD_FINISHED) || action.equals(ACTION_OPEN_DOWNLOADS_FINISHED)) { |                 if (action.equals(ACTION_RESET_DOWNLOAD_FINISHED) || action.equals(ACTION_OPEN_DOWNLOADS_FINISHED)) { | ||||||
|                     downloadDoneCount = 0; |                     downloadDoneCount = 0; | ||||||
| @@ -264,12 +257,12 @@ public class DownloadManagerService extends Service { | |||||||
|     @Override |     @Override | ||||||
|     public IBinder onBind(Intent intent) { |     public IBinder onBind(Intent intent) { | ||||||
|         int permissionCheck; |         int permissionCheck; | ||||||
|         if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { | //        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { | ||||||
|             permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE); | //            permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE); | ||||||
|             if (permissionCheck == PermissionChecker.PERMISSION_DENIED) { | //            if (permissionCheck == PermissionChecker.PERMISSION_DENIED) { | ||||||
|                 Toast.makeText(this, "Permission denied (read)", Toast.LENGTH_SHORT).show(); | //                Toast.makeText(this, "Permission denied (read)", Toast.LENGTH_SHORT).show(); | ||||||
|             } | //            } | ||||||
|         } | //        } | ||||||
|  |  | ||||||
|         permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE); |         permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE); | ||||||
|         if (permissionCheck == PermissionChecker.PERMISSION_DENIED) { |         if (permissionCheck == PermissionChecker.PERMISSION_DENIED) { | ||||||
| @@ -284,8 +277,8 @@ public class DownloadManagerService extends Service { | |||||||
|  |  | ||||||
|         switch (msg.what) { |         switch (msg.what) { | ||||||
|             case MESSAGE_FINISHED: |             case MESSAGE_FINISHED: | ||||||
|                 notifyMediaScanner(mission.getDownloadedFile()); |                 notifyMediaScanner(mission.storage.getUri()); | ||||||
|                 notifyFinishedDownload(mission.name); |                 notifyFinishedDownload(mission.storage.getName()); | ||||||
|                 mManager.setFinished(mission); |                 mManager.setFinished(mission); | ||||||
|                 handleConnectivityState(false); |                 handleConnectivityState(false); | ||||||
|                 updateForegroundState(mManager.runMissions()); |                 updateForegroundState(mManager.runMissions()); | ||||||
| @@ -344,7 +337,7 @@ public class DownloadManagerService extends Service { | |||||||
|         if (key.equals(getString(R.string.downloads_maximum_retry))) { |         if (key.equals(getString(R.string.downloads_maximum_retry))) { | ||||||
|             try { |             try { | ||||||
|                 String value = prefs.getString(key, getString(R.string.downloads_maximum_retry_default)); |                 String value = prefs.getString(key, getString(R.string.downloads_maximum_retry_default)); | ||||||
|                 mManager.mPrefMaxRetry = Integer.parseInt(value); |                 mManager.mPrefMaxRetry = value == null ? 0 : Integer.parseInt(value); | ||||||
|             } catch (Exception e) { |             } catch (Exception e) { | ||||||
|                 mManager.mPrefMaxRetry = 0; |                 mManager.mPrefMaxRetry = 0; | ||||||
|             } |             } | ||||||
| @@ -353,6 +346,12 @@ public class DownloadManagerService extends Service { | |||||||
|             mManager.mPrefMeteredDownloads = prefs.getBoolean(key, false); |             mManager.mPrefMeteredDownloads = prefs.getBoolean(key, false); | ||||||
|         } else if (key.equals(getString(R.string.downloads_queue_limit))) { |         } else if (key.equals(getString(R.string.downloads_queue_limit))) { | ||||||
|             mManager.mPrefQueueLimit = prefs.getBoolean(key, true); |             mManager.mPrefQueueLimit = prefs.getBoolean(key, true); | ||||||
|  |         } else if (key.equals(getString(R.string.downloads_storage_api))) { | ||||||
|  |             setupStorageAPI(false); | ||||||
|  |         } else if (key.equals(getString(R.string.download_path_video_key))) { | ||||||
|  |             loadMainStorage(key, DownloadManager.TAG_VIDEO, false); | ||||||
|  |         } else if (key.equals(getString(R.string.download_path_audio_key))) { | ||||||
|  |             loadMainStorage(key, DownloadManager.TAG_AUDIO, false); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -370,43 +369,61 @@ public class DownloadManagerService extends Service { | |||||||
|         mForeground = state; |         mForeground = state; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static void startMission(Context context, String urls[], String location, String name, char kind, |     /** | ||||||
|  |      * Start a new download mission | ||||||
|  |      * | ||||||
|  |      * @param context    the activity context | ||||||
|  |      * @param urls       the list of urls to download | ||||||
|  |      * @param storage    where the file is saved | ||||||
|  |      * @param kind       type of file (a: audio  v: video  s: subtitle ?: file-extension defined) | ||||||
|  |      * @param threads    the number of threads maximal used to download chunks of the file. | ||||||
|  |      * @param psName     the name of the required post-processing algorithm, or {@code null} to ignore. | ||||||
|  |      * @param source     source url of the resource | ||||||
|  |      * @param psArgs     the arguments for the post-processing algorithm. | ||||||
|  |      * @param nearLength the approximated final length of the file | ||||||
|  |      */ | ||||||
|  |     public static void startMission(Context context, String urls[], StoredFileHelper storage, char kind, | ||||||
|                                     int threads, String source, String psName, String[] psArgs, long nearLength) { |                                     int threads, String source, String psName, String[] psArgs, long nearLength) { | ||||||
|         Intent intent = new Intent(context, DownloadManagerService.class); |         Intent intent = new Intent(context, DownloadManagerService.class); | ||||||
|         intent.setAction(Intent.ACTION_RUN); |         intent.setAction(Intent.ACTION_RUN); | ||||||
|         intent.putExtra(EXTRA_URLS, urls); |         intent.putExtra(EXTRA_URLS, urls); | ||||||
|         intent.putExtra(EXTRA_NAME, name); |         intent.putExtra(EXTRA_PATH, storage.getUri()); | ||||||
|         intent.putExtra(EXTRA_LOCATION, location); |  | ||||||
|         intent.putExtra(EXTRA_KIND, kind); |         intent.putExtra(EXTRA_KIND, kind); | ||||||
|         intent.putExtra(EXTRA_THREADS, threads); |         intent.putExtra(EXTRA_THREADS, threads); | ||||||
|         intent.putExtra(EXTRA_SOURCE, source); |         intent.putExtra(EXTRA_SOURCE, source); | ||||||
|         intent.putExtra(EXTRA_POSTPROCESSING_NAME, psName); |         intent.putExtra(EXTRA_POSTPROCESSING_NAME, psName); | ||||||
|         intent.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs); |         intent.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs); | ||||||
|         intent.putExtra(EXTRA_NEAR_LENGTH, nearLength); |         intent.putExtra(EXTRA_NEAR_LENGTH, nearLength); | ||||||
|  |         intent.putExtra(EXTRA_MAIN_STORAGE_TAG, storage.getTag()); | ||||||
|         context.startService(intent); |         context.startService(intent); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static void checkForRunningMission(Context context, String location, String name, DMChecker checker) { |     public void startMission(Intent intent) { | ||||||
|         Intent intent = new Intent(); |         String[] urls = intent.getStringArrayExtra(EXTRA_URLS); | ||||||
|         intent.setClass(context, DownloadManagerService.class); |         Uri path = intent.getParcelableExtra(EXTRA_PATH); | ||||||
|         context.startService(intent); |         int threads = intent.getIntExtra(EXTRA_THREADS, 1); | ||||||
|  |         char kind = intent.getCharExtra(EXTRA_KIND, '?'); | ||||||
|  |         String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME); | ||||||
|  |         String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS); | ||||||
|  |         String source = intent.getStringExtra(EXTRA_SOURCE); | ||||||
|  |         long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0); | ||||||
|  |         String tag = intent.getStringExtra(EXTRA_MAIN_STORAGE_TAG); | ||||||
|  |  | ||||||
|         context.bindService(intent, new ServiceConnection() { |         StoredFileHelper storage; | ||||||
|             @Override |  | ||||||
|             public void onServiceConnected(ComponentName cname, IBinder service) { |  | ||||||
|         try { |         try { | ||||||
|                     ((DMBinder) service).getDownloadManager().checkForRunningMission(location, name, checker); |             storage = new StoredFileHelper(this, path, tag); | ||||||
|                 } catch (Exception err) { |         } catch (IOException e) { | ||||||
|                     Log.w(TAG, "checkForRunningMission() callback is defective", err); |             throw new RuntimeException(e);// this never should happen | ||||||
|         } |         } | ||||||
|  |  | ||||||
|                 context.unbindService(this); |         final DownloadMission mission = new DownloadMission(urls, storage, kind, Postprocessing.getAlgorithm(psName, psArgs)); | ||||||
|             } |         mission.threadCount = threads; | ||||||
|  |         mission.source = source; | ||||||
|  |         mission.nearLength = nearLength; | ||||||
|  |  | ||||||
|             @Override |         handleConnectivityState(true);// first check the actual network status | ||||||
|             public void onServiceDisconnected(ComponentName name) { |  | ||||||
|             } |         mManager.startMission(mission); | ||||||
|         }, Context.BIND_AUTO_CREATE); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void notifyFinishedDownload(String name) { |     public void notifyFinishedDownload(String name) { | ||||||
| @@ -471,12 +488,12 @@ public class DownloadManagerService extends Service { | |||||||
|         if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { |         if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { | ||||||
|             downloadFailedNotification.setContentTitle(getString(R.string.app_name)); |             downloadFailedNotification.setContentTitle(getString(R.string.app_name)); | ||||||
|             downloadFailedNotification.setStyle(new NotificationCompat.BigTextStyle() |             downloadFailedNotification.setStyle(new NotificationCompat.BigTextStyle() | ||||||
|                     .bigText(getString(R.string.download_failed).concat(": ").concat(mission.name))); |                     .bigText(getString(R.string.download_failed).concat(": ").concat(mission.storage.getName()))); | ||||||
|         } else { |         } else { | ||||||
|             downloadFailedNotification.setContentTitle(getString(R.string.download_failed)); |             downloadFailedNotification.setContentTitle(getString(R.string.download_failed)); | ||||||
|             downloadFailedNotification.setContentText(mission.name); |             downloadFailedNotification.setContentText(mission.storage.getName()); | ||||||
|             downloadFailedNotification.setStyle(new NotificationCompat.BigTextStyle() |             downloadFailedNotification.setStyle(new NotificationCompat.BigTextStyle() | ||||||
|                     .bigText(mission.name)); |                     .bigText(mission.storage.getName())); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         mNotificationManager.notify(id, downloadFailedNotification.build()); |         mNotificationManager.notify(id, downloadFailedNotification.build()); | ||||||
| @@ -508,16 +525,81 @@ public class DownloadManagerService extends Service { | |||||||
|         mLockAcquired = acquire; |         mLockAcquired = acquire; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     private void setupStorageAPI(boolean acquire) { | ||||||
|  |         loadMainStorage(getString(R.string.download_path_audio_key), DownloadManager.TAG_VIDEO, acquire); | ||||||
|  |         loadMainStorage(getString(R.string.download_path_video_key), DownloadManager.TAG_AUDIO, acquire); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     void loadMainStorage(String prefKey, String tag, boolean acquire) { | ||||||
|  |         String path = mPrefs.getString(prefKey, null); | ||||||
|  |  | ||||||
|  |         final String JAVA_IO = getString(R.string.downloads_storage_api_default); | ||||||
|  |         boolean useJavaIO = JAVA_IO.equals(mPrefs.getString(getString(R.string.downloads_storage_api), JAVA_IO)); | ||||||
|  |  | ||||||
|  |         final String defaultPath; | ||||||
|  |         if (tag.equals(DownloadManager.TAG_VIDEO)) | ||||||
|  |             defaultPath = Environment.DIRECTORY_MOVIES; | ||||||
|  |         else// if (tag.equals(DownloadManager.TAG_AUDIO)) | ||||||
|  |             defaultPath = Environment.DIRECTORY_MUSIC; | ||||||
|  |  | ||||||
|  |         StoredDirectoryHelper mainStorage; | ||||||
|  |         if (path == null || path.isEmpty()) { | ||||||
|  |             mainStorage = useJavaIO ? new StoredDirectoryHelper(defaultPath, tag) : null; | ||||||
|  |         } else { | ||||||
|  |  | ||||||
|  |             if (path.charAt(0) == File.separatorChar) { | ||||||
|  |                 Log.i(TAG, "Migrating old save path: " + path); | ||||||
|  |  | ||||||
|  |                 useJavaIO = true; | ||||||
|  |                 path = Uri.fromFile(new File(path)).toString(); | ||||||
|  |  | ||||||
|  |                 mPrefs.edit().putString(prefKey, path).apply(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (useJavaIO) { | ||||||
|  |                 mainStorage = new StoredDirectoryHelper(path, tag); | ||||||
|  |             } else { | ||||||
|  |  | ||||||
|  |                 // tree api is not available in older versions | ||||||
|  |                 if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { | ||||||
|  |                     mainStorage = null; | ||||||
|  |                 } else { | ||||||
|  |                     try { | ||||||
|  |                         mainStorage = new StoredDirectoryHelper(this, Uri.parse(path), tag); | ||||||
|  |                         if (acquire) mainStorage.acquirePermissions(); | ||||||
|  |                     } catch (IOException e) { | ||||||
|  |                         Log.e(TAG, "Failed to load the storage of " + tag + " from path: " + path, e); | ||||||
|  |                         mainStorage = null; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (tag.equals(DownloadManager.TAG_VIDEO)) | ||||||
|  |             mManager.mMainStorageVideo = mainStorage; | ||||||
|  |         else// if (tag.equals(DownloadManager.TAG_AUDIO)) | ||||||
|  |             mManager.mMainStorageAudio = mainStorage; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     //////////////////////////////////////////////////////////////////////////////////////////////// |     //////////////////////////////////////////////////////////////////////////////////////////////// | ||||||
|     // Wrappers for DownloadManager |     // Wrappers for DownloadManager | ||||||
|     //////////////////////////////////////////////////////////////////////////////////////////////// |     //////////////////////////////////////////////////////////////////////////////////////////////// | ||||||
|  |  | ||||||
|     public class DMBinder extends Binder { |     public class DownloadManagerBinder extends Binder { | ||||||
|         public DownloadManager getDownloadManager() { |         public DownloadManager getDownloadManager() { | ||||||
|             return mManager; |             return mManager; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         @Nullable | ||||||
|  |         public StoredDirectoryHelper getMainStorageVideo() { | ||||||
|  |             return mManager.mMainStorageVideo; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         @Nullable | ||||||
|  |         public StoredDirectoryHelper getMainStorageAudio() { | ||||||
|  |             return mManager.mMainStorageAudio; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         public void addMissionEventListener(Handler handler) { |         public void addMissionEventListener(Handler handler) { | ||||||
|             manageObservers(handler, true); |             manageObservers(handler, true); | ||||||
|         } |         } | ||||||
| @@ -548,10 +630,4 @@ public class DownloadManagerService extends Service { | |||||||
|  |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public interface DMChecker { |  | ||||||
|         void callback(MissionCheck result); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public enum MissionCheck {None, Pending, PendingRunning, Finished} |  | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,5 @@ | |||||||
|  | package us.shandian.giga.service; | ||||||
|  |  | ||||||
|  | public enum MissionState { | ||||||
|  |     None, Pending, PendingRunning, Finished | ||||||
|  | } | ||||||
| @@ -8,7 +8,6 @@ import android.content.Intent; | |||||||
| import android.net.Uri; | import android.net.Uri; | ||||||
| import android.os.AsyncTask; | import android.os.AsyncTask; | ||||||
| import android.os.Build; | import android.os.Build; | ||||||
| import android.os.Bundle; |  | ||||||
| import android.os.Handler; | import android.os.Handler; | ||||||
| import android.os.Looper; | import android.os.Looper; | ||||||
| import android.os.Message; | import android.os.Message; | ||||||
| @@ -49,6 +48,7 @@ import java.util.Collections; | |||||||
| import us.shandian.giga.get.DownloadMission; | import us.shandian.giga.get.DownloadMission; | ||||||
| import us.shandian.giga.get.FinishedMission; | import us.shandian.giga.get.FinishedMission; | ||||||
| import us.shandian.giga.get.Mission; | import us.shandian.giga.get.Mission; | ||||||
|  | import us.shandian.giga.io.StoredFileHelper; | ||||||
| import us.shandian.giga.service.DownloadManager; | import us.shandian.giga.service.DownloadManager; | ||||||
| import us.shandian.giga.service.DownloadManagerService; | import us.shandian.giga.service.DownloadManagerService; | ||||||
| import us.shandian.giga.ui.common.Deleter; | import us.shandian.giga.ui.common.Deleter; | ||||||
| @@ -69,6 +69,7 @@ import static us.shandian.giga.get.DownloadMission.ERROR_PERMISSION_DENIED; | |||||||
| import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING; | import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING; | ||||||
| import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD; | import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD; | ||||||
| import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_STOPPED; | import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_STOPPED; | ||||||
|  | import static us.shandian.giga.get.DownloadMission.ERROR_PROGRESS_LOST; | ||||||
| import static us.shandian.giga.get.DownloadMission.ERROR_SSL_EXCEPTION; | import static us.shandian.giga.get.DownloadMission.ERROR_SSL_EXCEPTION; | ||||||
| import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; | import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; | ||||||
| import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST; | import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST; | ||||||
| @@ -97,8 +98,9 @@ public class MissionAdapter extends Adapter<ViewHolder> { | |||||||
|     private MenuItem mStartButton; |     private MenuItem mStartButton; | ||||||
|     private MenuItem mPauseButton; |     private MenuItem mPauseButton; | ||||||
|     private View mEmptyMessage; |     private View mEmptyMessage; | ||||||
|  |     private RecoverHelper mRecover; | ||||||
|  |  | ||||||
|     public MissionAdapter(Context context, DownloadManager downloadManager, View emptyMessage) { |     public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage) { | ||||||
|         mContext = context; |         mContext = context; | ||||||
|         mDownloadManager = downloadManager; |         mDownloadManager = downloadManager; | ||||||
|         mDeleter = null; |         mDeleter = null; | ||||||
| @@ -156,7 +158,11 @@ public class MissionAdapter extends Adapter<ViewHolder> { | |||||||
|  |  | ||||||
|         if (h.item.mission instanceof DownloadMission) { |         if (h.item.mission instanceof DownloadMission) { | ||||||
|             mPendingDownloadsItems.remove(h); |             mPendingDownloadsItems.remove(h); | ||||||
|             if (mPendingDownloadsItems.size() < 1) setAutoRefresh(false); |             if (mPendingDownloadsItems.size() < 1) { | ||||||
|  |                 setAutoRefresh(false); | ||||||
|  |                 if (mStartButton != null) mStartButton.setVisible(false); | ||||||
|  |                 if (mPauseButton != null) mPauseButton.setVisible(false); | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         h.popupMenu.dismiss(); |         h.popupMenu.dismiss(); | ||||||
| @@ -189,10 +195,10 @@ public class MissionAdapter extends Adapter<ViewHolder> { | |||||||
|         ViewHolderItem h = (ViewHolderItem) view; |         ViewHolderItem h = (ViewHolderItem) view; | ||||||
|         h.item = item; |         h.item = item; | ||||||
|  |  | ||||||
|         Utility.FileType type = Utility.getFileType(item.mission.kind, item.mission.name); |         Utility.FileType type = Utility.getFileType(item.mission.kind, item.mission.storage.getName()); | ||||||
|  |  | ||||||
|         h.icon.setImageResource(Utility.getIconForFileType(type)); |         h.icon.setImageResource(Utility.getIconForFileType(type)); | ||||||
|         h.name.setText(item.mission.name); |         h.name.setText(item.mission.storage.getName()); | ||||||
|  |  | ||||||
|         h.progress.setColors(Utility.getBackgroundForFileType(mContext, type), Utility.getForegroundForFileType(mContext, type)); |         h.progress.setColors(Utility.getBackgroundForFileType(mContext, type), Utility.getForegroundForFileType(mContext, type)); | ||||||
|  |  | ||||||
| @@ -273,7 +279,7 @@ public class MissionAdapter extends Adapter<ViewHolder> { | |||||||
|         long length = mission.getLength(); |         long length = mission.getLength(); | ||||||
|  |  | ||||||
|         int state; |         int state; | ||||||
|         if (mission.isPsFailed()) { |         if (mission.isPsFailed() || mission.errCode == ERROR_POSTPROCESSING_HOLD) { | ||||||
|             state = 0; |             state = 0; | ||||||
|         } else if (!mission.running) { |         } else if (!mission.running) { | ||||||
|             state = mission.enqueued ? 1 : 2; |             state = mission.enqueued ? 1 : 2; | ||||||
| @@ -334,11 +340,17 @@ public class MissionAdapter extends Adapter<ViewHolder> { | |||||||
|         if (BuildConfig.DEBUG) |         if (BuildConfig.DEBUG) | ||||||
|             Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider"); |             Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider"); | ||||||
|  |  | ||||||
|         Uri uri = FileProvider.getUriForFile( |         Uri uri; | ||||||
|  |  | ||||||
|  |         if (mission.storage.isDirect()) { | ||||||
|  |             uri = FileProvider.getUriForFile( | ||||||
|                     mContext, |                     mContext, | ||||||
|                     BuildConfig.APPLICATION_ID + ".provider", |                     BuildConfig.APPLICATION_ID + ".provider", | ||||||
|                 mission.getDownloadedFile() |                     mission.storage.getIOFile() | ||||||
|             ); |             ); | ||||||
|  |         } else { | ||||||
|  |             uri = mission.storage.getUri(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         Intent intent = new Intent(); |         Intent intent = new Intent(); | ||||||
|         intent.setAction(Intent.ACTION_VIEW); |         intent.setAction(Intent.ACTION_VIEW); | ||||||
| @@ -366,13 +378,13 @@ public class MissionAdapter extends Adapter<ViewHolder> { | |||||||
|  |  | ||||||
|         Intent intent = new Intent(Intent.ACTION_SEND); |         Intent intent = new Intent(Intent.ACTION_SEND); | ||||||
|         intent.setType(resolveMimeType(mission)); |         intent.setType(resolveMimeType(mission)); | ||||||
|         intent.putExtra(Intent.EXTRA_STREAM, mission.getDownloadedFile().toURI()); |         intent.putExtra(Intent.EXTRA_STREAM, mission.storage.getUri()); | ||||||
|  |  | ||||||
|         mContext.startActivity(Intent.createChooser(intent, null)); |         mContext.startActivity(Intent.createChooser(intent, null)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private static String resolveMimeType(@NonNull Mission mission) { |     private static String resolveMimeType(@NonNull Mission mission) { | ||||||
|         String ext = Utility.getFileExt(mission.getDownloadedFile().getName()); |         String ext = Utility.getFileExt(mission.storage.getName()); | ||||||
|         if (ext == null) return DEFAULT_MIME_TYPE; |         if (ext == null) return DEFAULT_MIME_TYPE; | ||||||
|  |  | ||||||
|         String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1)); |         String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1)); | ||||||
| @@ -381,7 +393,7 @@ public class MissionAdapter extends Adapter<ViewHolder> { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     private boolean checkInvalidFile(@NonNull Mission mission) { |     private boolean checkInvalidFile(@NonNull Mission mission) { | ||||||
|         if (mission.getDownloadedFile().exists()) return false; |         if (mission.storage.existsAsFile()) return false; | ||||||
|  |  | ||||||
|         Toast.makeText(mContext, R.string.missing_file, Toast.LENGTH_SHORT).show(); |         Toast.makeText(mContext, R.string.missing_file, Toast.LENGTH_SHORT).show(); | ||||||
|         return true; |         return true; | ||||||
| @@ -462,6 +474,8 @@ public class MissionAdapter extends Adapter<ViewHolder> { | |||||||
|             case ERROR_UNKNOWN_EXCEPTION: |             case ERROR_UNKNOWN_EXCEPTION: | ||||||
|                 showError(mission.errObject, UserAction.DOWNLOAD_FAILED, R.string.general_error); |                 showError(mission.errObject, UserAction.DOWNLOAD_FAILED, R.string.general_error); | ||||||
|                 return; |                 return; | ||||||
|  |             case ERROR_PROGRESS_LOST: | ||||||
|  |                 msg = R.string.error_progress_lost; | ||||||
|             default: |             default: | ||||||
|                 if (mission.errCode >= 100 && mission.errCode < 600) { |                 if (mission.errCode >= 100 && mission.errCode < 600) { | ||||||
|                     msgEx = "HTTP " + mission.errCode; |                     msgEx = "HTTP " + mission.errCode; | ||||||
| @@ -490,7 +504,7 @@ public class MissionAdapter extends Adapter<ViewHolder> { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         builder.setNegativeButton(android.R.string.ok, (dialog, which) -> dialog.cancel()) |         builder.setNegativeButton(android.R.string.ok, (dialog, which) -> dialog.cancel()) | ||||||
|                 .setTitle(mission.name) |                 .setTitle(mission.storage.getName()) | ||||||
|                 .create() |                 .create() | ||||||
|                 .show(); |                 .show(); | ||||||
|     } |     } | ||||||
| @@ -539,6 +553,10 @@ public class MissionAdapter extends Adapter<ViewHolder> { | |||||||
|                     updateProgress(h); |                     updateProgress(h); | ||||||
|                     return true; |                     return true; | ||||||
|                 case R.id.retry: |                 case R.id.retry: | ||||||
|  |                     if (mission.hasInvalidStorage()) { | ||||||
|  |                         mRecover.tryRecover(mission); | ||||||
|  |                         return true; | ||||||
|  |                     } | ||||||
|                     mission.psContinue(true); |                     mission.psContinue(true); | ||||||
|                     return true; |                     return true; | ||||||
|                 case R.id.cancel: |                 case R.id.cancel: | ||||||
| @@ -561,7 +579,7 @@ public class MissionAdapter extends Adapter<ViewHolder> { | |||||||
|                 return true; |                 return true; | ||||||
|             case R.id.md5: |             case R.id.md5: | ||||||
|             case R.id.sha1: |             case R.id.sha1: | ||||||
|                 new ChecksumTask(mContext).execute(h.item.mission.getDownloadedFile().getAbsolutePath(), ALGORITHMS.get(id)); |                 new ChecksumTask(mContext).execute(h.item.mission.storage, ALGORITHMS.get(id)); | ||||||
|                 return true; |                 return true; | ||||||
|             case R.id.source: |             case R.id.source: | ||||||
|                 /*Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(h.item.mission.source)); |                 /*Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(h.item.mission.source)); | ||||||
| @@ -641,19 +659,38 @@ public class MissionAdapter extends Adapter<ViewHolder> { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     public void deleterDispose(Bundle bundle) { |     public void deleterDispose(boolean commitChanges) { | ||||||
|         if (mDeleter != null) mDeleter.dispose(bundle); |         if (mDeleter != null) mDeleter.dispose(commitChanges); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void deleterLoad(Bundle bundle, View view) { |     public void deleterLoad(View view) { | ||||||
|         if (mDeleter == null) |         if (mDeleter == null) | ||||||
|             mDeleter = new Deleter(bundle, view, mContext, this, mDownloadManager, mIterator, mHandler); |             mDeleter = new Deleter(view, mContext, this, mDownloadManager, mIterator, mHandler); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void deleterResume() { |     public void deleterResume() { | ||||||
|         if (mDeleter != null) mDeleter.resume(); |         if (mDeleter != null) mDeleter.resume(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public void recoverMission(DownloadMission mission, StoredFileHelper newStorage) { | ||||||
|  |         for (ViewHolderItem h : mPendingDownloadsItems) { | ||||||
|  |             if (mission != h.item.mission) continue; | ||||||
|  |  | ||||||
|  |             mission.changeStorage(newStorage); | ||||||
|  |             mission.errCode = DownloadMission.ERROR_NOTHING; | ||||||
|  |             mission.errObject = null; | ||||||
|  |  | ||||||
|  |             h.status.setText(UNDEFINED_PROGRESS); | ||||||
|  |             h.state = -1; | ||||||
|  |             h.size.setText(Utility.formatBytes(mission.getLength())); | ||||||
|  |             h.progress.setMarquee(true); | ||||||
|  |  | ||||||
|  |             mDownloadManager.resumeMission(mission); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     private boolean mUpdaterRunning = false; |     private boolean mUpdaterRunning = false; | ||||||
|     private final Runnable rUpdater = this::updater; |     private final Runnable rUpdater = this::updater; | ||||||
| @@ -695,6 +732,10 @@ public class MissionAdapter extends Adapter<ViewHolder> { | |||||||
|         return Float.isNaN(value) || Float.isInfinite(value); |         return Float.isNaN(value) || Float.isInfinite(value); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public void setRecover(@NonNull RecoverHelper callback) { | ||||||
|  |         mRecover = callback; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     class ViewHolderItem extends RecyclerView.ViewHolder { |     class ViewHolderItem extends RecyclerView.ViewHolder { | ||||||
|         DownloadManager.MissionItem item; |         DownloadManager.MissionItem item; | ||||||
| @@ -780,7 +821,11 @@ public class MissionAdapter extends Adapter<ViewHolder> { | |||||||
|             DownloadMission mission = item.mission instanceof DownloadMission ? (DownloadMission) item.mission : null; |             DownloadMission mission = item.mission instanceof DownloadMission ? (DownloadMission) item.mission : null; | ||||||
|  |  | ||||||
|             if (mission != null) { |             if (mission != null) { | ||||||
|                 if (mission.isPsRunning()) { |                 if (mission.hasInvalidStorage()) { | ||||||
|  |                     retry.setEnabled(true); | ||||||
|  |                     delete.setEnabled(true); | ||||||
|  |                     showError.setEnabled(true); | ||||||
|  |                 } else if (mission.isPsRunning()) { | ||||||
|                     switch (mission.errCode) { |                     switch (mission.errCode) { | ||||||
|                         case ERROR_INSUFFICIENT_STORAGE: |                         case ERROR_INSUFFICIENT_STORAGE: | ||||||
|                         case ERROR_POSTPROCESSING_HOLD: |                         case ERROR_POSTPROCESSING_HOLD: | ||||||
| @@ -838,7 +883,7 @@ public class MissionAdapter extends Adapter<ViewHolder> { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     static class ChecksumTask extends AsyncTask<String, Void, String> { |     static class ChecksumTask extends AsyncTask<Object, Void, String> { | ||||||
|         ProgressDialog progressDialog; |         ProgressDialog progressDialog; | ||||||
|         WeakReference<Activity> weakReference; |         WeakReference<Activity> weakReference; | ||||||
|  |  | ||||||
| @@ -861,8 +906,8 @@ public class MissionAdapter extends Adapter<ViewHolder> { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
|         protected String doInBackground(String... params) { |         protected String doInBackground(Object... params) { | ||||||
|             return Utility.checksum(params[0], params[1]); |             return Utility.checksum((StoredFileHelper) params[0], (String) params[1]); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
| @@ -889,4 +934,8 @@ public class MissionAdapter extends Adapter<ViewHolder> { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public interface RecoverHelper { | ||||||
|  |         void tryRecover(DownloadMission mission); | ||||||
|  |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,8 +3,6 @@ package us.shandian.giga.ui.common; | |||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.content.Intent; | import android.content.Intent; | ||||||
| import android.graphics.Color; | import android.graphics.Color; | ||||||
| import android.net.Uri; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.os.Handler; | import android.os.Handler; | ||||||
| import android.support.design.widget.Snackbar; | import android.support.design.widget.Snackbar; | ||||||
| import android.view.View; | import android.view.View; | ||||||
| @@ -23,8 +21,6 @@ public class Deleter { | |||||||
|     private static final int TIMEOUT = 5000;// ms |     private static final int TIMEOUT = 5000;// ms | ||||||
|     private static final int DELAY = 350;// ms |     private static final int DELAY = 350;// ms | ||||||
|     private static final int DELAY_RESUME = 400;// ms |     private static final int DELAY_RESUME = 400;// 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 Snackbar snackbar; | ||||||
|     private ArrayList<Mission> items; |     private ArrayList<Mission> items; | ||||||
| @@ -41,7 +37,7 @@ public class Deleter { | |||||||
|     private final Runnable rNext; |     private final Runnable rNext; | ||||||
|     private final Runnable rCommit; |     private final Runnable rCommit; | ||||||
|  |  | ||||||
|     public Deleter(Bundle b, View v, Context c, MissionAdapter a, DownloadManager d, MissionIterator i, Handler h) { |     public Deleter(View v, Context c, MissionAdapter a, DownloadManager d, MissionIterator i, Handler h) { | ||||||
|         mView = v; |         mView = v; | ||||||
|         mContext = c; |         mContext = c; | ||||||
|         mAdapter = a; |         mAdapter = a; | ||||||
| @@ -55,27 +51,6 @@ public class Deleter { | |||||||
|         rCommit = this::commit; |         rCommit = this::commit; | ||||||
|  |  | ||||||
|         items = new ArrayList<>(2); |         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) { |     public void append(Mission item) { | ||||||
| @@ -104,7 +79,7 @@ public class Deleter { | |||||||
|     private void next() { |     private void next() { | ||||||
|         if (items.size() < 1) return; |         if (items.size() < 1) return; | ||||||
|  |  | ||||||
|         String msg = mContext.getString(R.string.file_deleted).concat(":\n").concat(items.get(0).name); |         String msg = mContext.getString(R.string.file_deleted).concat(":\n").concat(items.get(0).storage.getName()); | ||||||
|  |  | ||||||
|         snackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE); |         snackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE); | ||||||
|         snackbar.setAction(R.string.undo, s -> forget()); |         snackbar.setAction(R.string.undo, s -> forget()); | ||||||
| @@ -125,7 +100,7 @@ public class Deleter { | |||||||
|             mDownloadManager.deleteMission(mission); |             mDownloadManager.deleteMission(mission); | ||||||
|  |  | ||||||
|             if (mission instanceof FinishedMission) { |             if (mission instanceof FinishedMission) { | ||||||
|                 mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(mission.getDownloadedFile()))); |                 mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, mission.storage.getUri())); | ||||||
|             } |             } | ||||||
|             break; |             break; | ||||||
|         } |         } | ||||||
| @@ -151,27 +126,14 @@ public class Deleter { | |||||||
|         mHandler.postDelayed(rShow, DELAY_RESUME); |         mHandler.postDelayed(rShow, DELAY_RESUME); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void dispose(Bundle bundle) { |     public void dispose(boolean commitChanges) { | ||||||
|         if (items.size() < 1) return; |         if (items.size() < 1) return; | ||||||
|  |  | ||||||
|         pause(); |         pause(); | ||||||
|  |  | ||||||
|         if (bundle == null) { |         if (!commitChanges) return; | ||||||
|  |  | ||||||
|         for (Mission mission : items) mDownloadManager.deleteMission(mission); |         for (Mission mission : items) mDownloadManager.deleteMission(mission); | ||||||
|         items = null; |         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,7 +1,6 @@ | |||||||
| package us.shandian.giga.ui.fragment; | package us.shandian.giga.ui.fragment; | ||||||
|  |  | ||||||
| import android.app.Activity; | import android.app.Activity; | ||||||
| import android.app.Fragment; |  | ||||||
| import android.content.ComponentName; | import android.content.ComponentName; | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.content.Intent; | import android.content.Intent; | ||||||
| @@ -10,6 +9,7 @@ import android.content.SharedPreferences; | |||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
| import android.os.IBinder; | import android.os.IBinder; | ||||||
| import android.preference.PreferenceManager; | import android.preference.PreferenceManager; | ||||||
|  | import android.support.v4.app.Fragment; | ||||||
| import android.support.v7.widget.GridLayoutManager; | import android.support.v7.widget.GridLayoutManager; | ||||||
| import android.support.v7.widget.LinearLayoutManager; | import android.support.v7.widget.LinearLayoutManager; | ||||||
| import android.support.v7.widget.RecyclerView; | import android.support.v7.widget.RecyclerView; | ||||||
| @@ -18,18 +18,24 @@ import android.view.Menu; | |||||||
| import android.view.MenuItem; | import android.view.MenuItem; | ||||||
| import android.view.View; | import android.view.View; | ||||||
| import android.view.ViewGroup; | import android.view.ViewGroup; | ||||||
|  | import android.widget.Toast; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.util.ThemeHelper; | import org.schabi.newpipe.util.ThemeHelper; | ||||||
|  |  | ||||||
|  | import java.io.IOException; | ||||||
|  |  | ||||||
|  | import us.shandian.giga.get.DownloadMission; | ||||||
|  | import us.shandian.giga.io.StoredFileHelper; | ||||||
| import us.shandian.giga.service.DownloadManager; | import us.shandian.giga.service.DownloadManager; | ||||||
| import us.shandian.giga.service.DownloadManagerService; | import us.shandian.giga.service.DownloadManagerService; | ||||||
| import us.shandian.giga.service.DownloadManagerService.DMBinder; | import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder; | ||||||
| import us.shandian.giga.ui.adapter.MissionAdapter; | import us.shandian.giga.ui.adapter.MissionAdapter; | ||||||
|  |  | ||||||
| public class MissionsFragment extends Fragment { | public class MissionsFragment extends Fragment { | ||||||
|  |  | ||||||
|     private static final int SPAN_SIZE = 2; |     private static final int SPAN_SIZE = 2; | ||||||
|  |     private static final int REQUEST_DOWNLOAD_PATH_SAF = 0x1230; | ||||||
|  |  | ||||||
|     private SharedPreferences mPrefs; |     private SharedPreferences mPrefs; | ||||||
|     private boolean mLinear; |     private boolean mLinear; | ||||||
| @@ -45,24 +51,32 @@ public class MissionsFragment extends Fragment { | |||||||
|     private LinearLayoutManager mLinearManager; |     private LinearLayoutManager mLinearManager; | ||||||
|     private Context mContext; |     private Context mContext; | ||||||
|  |  | ||||||
|     private DMBinder mBinder; |     private DownloadManagerBinder mBinder; | ||||||
|     private Bundle mBundle; |  | ||||||
|     private boolean mForceUpdate; |     private boolean mForceUpdate; | ||||||
|  |  | ||||||
|  |     private DownloadMission unsafeMissionTarget = null; | ||||||
|  |  | ||||||
|     private ServiceConnection mConnection = new ServiceConnection() { |     private ServiceConnection mConnection = new ServiceConnection() { | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
|         public void onServiceConnected(ComponentName name, IBinder binder) { |         public void onServiceConnected(ComponentName name, IBinder binder) { | ||||||
|             mBinder = (DownloadManagerService.DMBinder) binder; |             mBinder = (DownloadManagerBinder) binder; | ||||||
|             mBinder.clearDownloadNotifications(); |             mBinder.clearDownloadNotifications(); | ||||||
|  |  | ||||||
|             mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty); |             mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty); | ||||||
|             mAdapter.deleterLoad(mBundle, getView()); |             mAdapter.deleterLoad(getView()); | ||||||
|  |  | ||||||
|  |             mAdapter.setRecover(mission -> | ||||||
|  |                     StoredFileHelper.requestSafWithFileCreation( | ||||||
|  |                             MissionsFragment.this, | ||||||
|  |                             REQUEST_DOWNLOAD_PATH_SAF, | ||||||
|  |                             mission.storage.getName(), | ||||||
|  |                             mission.storage.getType() | ||||||
|  |                     ) | ||||||
|  |             ); | ||||||
|  |  | ||||||
|             setAdapterButtons(); |             setAdapterButtons(); | ||||||
|  |  | ||||||
|             mBundle = null; |  | ||||||
|  |  | ||||||
|             mBinder.addMissionEventListener(mAdapter.getMessenger()); |             mBinder.addMissionEventListener(mAdapter.getMessenger()); | ||||||
|             mBinder.enableNotifications(false); |             mBinder.enableNotifications(false); | ||||||
|  |  | ||||||
| @@ -84,9 +98,6 @@ public class MissionsFragment extends Fragment { | |||||||
|         mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); |         mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); | ||||||
|         mLinear = mPrefs.getBoolean("linear", false); |         mLinear = mPrefs.getBoolean("linear", false); | ||||||
|  |  | ||||||
|         //mContext = getActivity().getApplicationContext(); |  | ||||||
|         mBundle = savedInstanceState; |  | ||||||
|  |  | ||||||
|         // Bind the service |         // Bind the service | ||||||
|         mContext.bindService(new Intent(mContext, DownloadManagerService.class), mConnection, Context.BIND_AUTO_CREATE); |         mContext.bindService(new Intent(mContext, DownloadManagerService.class), mConnection, Context.BIND_AUTO_CREATE); | ||||||
|  |  | ||||||
| @@ -148,7 +159,7 @@ public class MissionsFragment extends Fragment { | |||||||
|         mBinder.removeMissionEventListener(mAdapter.getMessenger()); |         mBinder.removeMissionEventListener(mAdapter.getMessenger()); | ||||||
|         mBinder.enableNotifications(true); |         mBinder.enableNotifications(true); | ||||||
|         mContext.unbindService(mConnection); |         mContext.unbindService(mConnection); | ||||||
|         mAdapter.deleterDispose(null); |         mAdapter.deleterDispose(true); | ||||||
|  |  | ||||||
|         mBinder = null; |         mBinder = null; | ||||||
|         mAdapter = null; |         mAdapter = null; | ||||||
| @@ -178,10 +189,12 @@ public class MissionsFragment extends Fragment { | |||||||
|                 return true; |                 return true; | ||||||
|             case R.id.start_downloads: |             case R.id.start_downloads: | ||||||
|                 item.setVisible(false); |                 item.setVisible(false); | ||||||
|  |                 mPause.setVisible(true); | ||||||
|                 mBinder.getDownloadManager().startAllMissions(); |                 mBinder.getDownloadManager().startAllMissions(); | ||||||
|                 return true; |                 return true; | ||||||
|             case R.id.pause_downloads: |             case R.id.pause_downloads: | ||||||
|                 item.setVisible(false); |                 item.setVisible(false); | ||||||
|  |                 mStart.setVisible(true); | ||||||
|                 mBinder.getDownloadManager().pauseAllMissions(false); |                 mBinder.getDownloadManager().pauseAllMissions(false); | ||||||
|                 mAdapter.ensurePausedMissions();// update items view |                 mAdapter.ensurePausedMissions();// update items view | ||||||
|             default: |             default: | ||||||
| @@ -231,7 +244,7 @@ public class MissionsFragment extends Fragment { | |||||||
|         super.onSaveInstanceState(outState); |         super.onSaveInstanceState(outState); | ||||||
|  |  | ||||||
|         if (mAdapter != null) { |         if (mAdapter != null) { | ||||||
|             mAdapter.deleterDispose(outState); |             mAdapter.deleterDispose(false); | ||||||
|             mForceUpdate = true; |             mForceUpdate = true; | ||||||
|             mBinder.removeMissionEventListener(mAdapter.getMessenger()); |             mBinder.removeMissionEventListener(mAdapter.getMessenger()); | ||||||
|         } |         } | ||||||
| @@ -260,4 +273,22 @@ public class MissionsFragment extends Fragment { | |||||||
|         if (mAdapter != null) mAdapter.onPaused(); |         if (mAdapter != null) mAdapter.onPaused(); | ||||||
|         if (mBinder != null) mBinder.enableNotifications(true); |         if (mBinder != null) mBinder.enableNotifications(true); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onActivityResult(int requestCode, int resultCode, Intent data) { | ||||||
|  |         super.onActivityResult(requestCode, resultCode, data); | ||||||
|  |  | ||||||
|  |         if (requestCode != REQUEST_DOWNLOAD_PATH_SAF || resultCode != Activity.RESULT_OK) return; | ||||||
|  |  | ||||||
|  |         if (unsafeMissionTarget == null || data.getData() == null) { | ||||||
|  |             return;// unsafeMissionTarget cannot be null | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             StoredFileHelper storage = new StoredFileHelper(mContext, data.getData(), unsafeMissionTarget.storage.getTag()); | ||||||
|  |             mAdapter.recoverMission(unsafeMissionTarget, storage); | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             Toast.makeText(mContext, R.string.general_error, Toast.LENGTH_LONG).show(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -12,11 +12,11 @@ import android.support.v4.content.ContextCompat; | |||||||
| import android.widget.Toast; | import android.widget.Toast; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
|  | import org.schabi.newpipe.streams.io.SharpStream; | ||||||
|  |  | ||||||
| import java.io.BufferedOutputStream; | import java.io.BufferedOutputStream; | ||||||
| import java.io.File; | import java.io.File; | ||||||
| import java.io.FileInputStream; | import java.io.FileInputStream; | ||||||
| import java.io.FileNotFoundException; |  | ||||||
| import java.io.FileOutputStream; | import java.io.FileOutputStream; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.io.ObjectInputStream; | import java.io.ObjectInputStream; | ||||||
| @@ -25,7 +25,8 @@ import java.io.Serializable; | |||||||
| import java.net.HttpURLConnection; | import java.net.HttpURLConnection; | ||||||
| import java.security.MessageDigest; | import java.security.MessageDigest; | ||||||
| import java.security.NoSuchAlgorithmException; | import java.security.NoSuchAlgorithmException; | ||||||
| import java.util.Locale; |  | ||||||
|  | import us.shandian.giga.io.StoredFileHelper; | ||||||
|  |  | ||||||
| public class Utility { | public class Utility { | ||||||
|  |  | ||||||
| @@ -206,7 +207,7 @@ public class Utility { | |||||||
|         Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show(); |         Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static String checksum(String path, String algorithm) { |     public static String checksum(StoredFileHelper source, String algorithm) { | ||||||
|         MessageDigest md; |         MessageDigest md; | ||||||
|  |  | ||||||
|         try { |         try { | ||||||
| @@ -215,11 +216,11 @@ public class Utility { | |||||||
|             throw new RuntimeException(e); |             throw new RuntimeException(e); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         FileInputStream i; |         SharpStream i; | ||||||
|  |  | ||||||
|         try { |         try { | ||||||
|             i = new FileInputStream(path); |             i = source.getStream(); | ||||||
|         } catch (FileNotFoundException e) { |         } catch (Exception e) { | ||||||
|             throw new RuntimeException(e); |             throw new RuntimeException(e); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -247,15 +248,15 @@ public class Utility { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     @SuppressWarnings("ResultOfMethodCallIgnored") |     @SuppressWarnings("ResultOfMethodCallIgnored") | ||||||
|     public static boolean mkdir(File path, boolean allDirs) { |     public static boolean mkdir(File p, boolean allDirs) { | ||||||
|         if (path.exists()) return true; |         if (p.exists()) return true; | ||||||
|  |  | ||||||
|         if (allDirs) |         if (allDirs) | ||||||
|             path.mkdirs(); |             p.mkdirs(); | ||||||
|         else |         else | ||||||
|             path.mkdir(); |             p.mkdir(); | ||||||
|  |  | ||||||
|         return path.exists(); |         return p.exists(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static long getContentLength(HttpURLConnection connection) { |     public static long getContentLength(HttpURLConnection connection) { | ||||||
|   | |||||||
| @@ -462,12 +462,12 @@ | |||||||
|     <string name="download_finished_more">%s أنتهى التحميل</string> |     <string name="download_finished_more">%s أنتهى التحميل</string> | ||||||
|     <string name="generate_unique_name">إنشاء اسم فريد</string> |     <string name="generate_unique_name">إنشاء اسم فريد</string> | ||||||
|     <string name="overwrite">الكتابة فوق</string> |     <string name="overwrite">الكتابة فوق</string> | ||||||
|     <string name="overwrite_warning">يوجد ملف تحميل بهذا الاسم موجود مسبقاً</string> |     <string name="overwrite_finished_warning">يوجد ملف تحميل بهذا الاسم موجود مسبقاً</string> | ||||||
|     <string name="download_already_running">هنالك تحميل قيد التقدم بهذا الاسم</string> |     <string name="download_already_running">هنالك تحميل قيد التقدم بهذا الاسم</string> | ||||||
|     <string name="show_error">إظهار خطأ</string> |     <string name="show_error">إظهار خطأ</string> | ||||||
|     <string name="label_code">كود</string> |     <string name="label_code">كود</string> | ||||||
|     <string name="error_path_creation">لا يمكن إنشاء الملف</string> |     <string name="error_file_creation">لا يمكن إنشاء الملف</string> | ||||||
|     <string name="error_file_creation">لا يمكن إنشاء المجلد الوجهة</string> |     <string name="error_path_creation">لا يمكن إنشاء المجلد الوجهة</string> | ||||||
|     <string name="error_permission_denied">تم رفضها من قبل النظام</string> |     <string name="error_permission_denied">تم رفضها من قبل النظام</string> | ||||||
|     <string name="error_ssl_exception">فشل اتصال الأمن</string> |     <string name="error_ssl_exception">فشل اتصال الأمن</string> | ||||||
|     <string name="error_unknown_host">تعذر العثور على الخادم</string> |     <string name="error_unknown_host">تعذر العثور على الخادم</string> | ||||||
|   | |||||||
| @@ -432,8 +432,8 @@ | |||||||
|     <string name="generate_unique_name">Genera un nom únic</string> |     <string name="generate_unique_name">Genera un nom únic</string> | ||||||
|     <string name="show_error">Mostra l\'error</string> |     <string name="show_error">Mostra l\'error</string> | ||||||
|     <string name="label_code">Codi</string> |     <string name="label_code">Codi</string> | ||||||
|     <string name="error_path_creation">No es pot crear el fitxer</string> |     <string name="error_file_creation">No es pot crear el fitxer</string> | ||||||
|     <string name="error_file_creation">No es pot crear la carpeta de destinació</string> |     <string name="error_path_creation">No es pot crear la carpeta de destinació</string> | ||||||
|     <string name="stop">Atura</string> |     <string name="stop">Atura</string> | ||||||
|     <string name="events">Esdeveniments</string> |     <string name="events">Esdeveniments</string> | ||||||
|     <string name="app_update_notification_channel_description">Notificacions de noves versions del NewPipe</string> |     <string name="app_update_notification_channel_description">Notificacions de noves versions del NewPipe</string> | ||||||
|   | |||||||
| @@ -437,11 +437,11 @@ | |||||||
|     <string name="download_finished_more">%s已下载完毕</string> |     <string name="download_finished_more">%s已下载完毕</string> | ||||||
|     <string name="generate_unique_name">生成独特的名字</string> |     <string name="generate_unique_name">生成独特的名字</string> | ||||||
|     <string name="overwrite">覆写</string> |     <string name="overwrite">覆写</string> | ||||||
|     <string name="overwrite_warning">同名的已下载文件已经存在</string> |     <string name="overwrite_finished_warning">同名的已下载文件已经存在</string> | ||||||
|     <string name="download_already_running">同名下载进行中</string> |     <string name="download_already_running">同名下载进行中</string> | ||||||
|     <string name="show_error">显示错误</string> |     <string name="show_error">显示错误</string> | ||||||
|     <string name="label_code">代码</string> |     <string name="label_code">代码</string> | ||||||
|     <string name="error_path_creation">无法创建该文件</string> |     <string name="error_file_creation">无法创建该文件</string> | ||||||
|     <string name="error_permission_denied">系统拒绝此批准</string> |     <string name="error_permission_denied">系统拒绝此批准</string> | ||||||
|     <string name="error_ssl_exception">安全连接失败</string> |     <string name="error_ssl_exception">安全连接失败</string> | ||||||
|     <string name="error_unknown_host">找不到服务器</string> |     <string name="error_unknown_host">找不到服务器</string> | ||||||
| @@ -464,7 +464,7 @@ | |||||||
|     <string name="grid">网格</string> |     <string name="grid">网格</string> | ||||||
|     <string name="switch_view">切换视图</string> |     <string name="switch_view">切换视图</string> | ||||||
|     <string name="app_update_notification_content_title">NewPipe 更新可用!</string> |     <string name="app_update_notification_content_title">NewPipe 更新可用!</string> | ||||||
|     <string name="error_file_creation">无法创建目标文件夹</string> |     <string name="error_path_creation">无法创建目标文件夹</string> | ||||||
|     <string name="error_http_unsupported_range">服务器不接受多线程下载, 请重试使用 @string/msg_threads = 1</string> |     <string name="error_http_unsupported_range">服务器不接受多线程下载, 请重试使用 @string/msg_threads = 1</string> | ||||||
|     <string name="error_http_requested_range_not_satisfiable">请求范围无法满足</string> |     <string name="error_http_requested_range_not_satisfiable">请求范围无法满足</string> | ||||||
|     <string name="msg_pending_downloads">继续进行%s个待下载转移</string> |     <string name="msg_pending_downloads">继续进行%s个待下载转移</string> | ||||||
|   | |||||||
| @@ -372,8 +372,8 @@ | |||||||
|     <string name="download_already_running">Der er en download i gang med dette navn</string> |     <string name="download_already_running">Der er en download i gang med dette navn</string> | ||||||
|     <string name="show_error">Vis fejl</string> |     <string name="show_error">Vis fejl</string> | ||||||
|     <string name="label_code">Kode</string> |     <string name="label_code">Kode</string> | ||||||
|     <string name="error_path_creation">Filen kan ikke oprettes</string> |     <string name="error_file_creation">Filen kan ikke oprettes</string> | ||||||
|     <string name="error_file_creation">Destinationsmappen kan ikke oprettes</string> |     <string name="error_path_creation">Destinationsmappen kan ikke oprettes</string> | ||||||
|     <string name="error_permission_denied">Adgang nægtet af systemet</string> |     <string name="error_permission_denied">Adgang nægtet af systemet</string> | ||||||
|     <string name="error_ssl_exception">Sikker forbindelse fejlede</string> |     <string name="error_ssl_exception">Sikker forbindelse fejlede</string> | ||||||
|     <string name="error_unknown_host">Kunne ikke finde serveren</string> |     <string name="error_unknown_host">Kunne ikke finde serveren</string> | ||||||
|   | |||||||
| @@ -448,12 +448,12 @@ | |||||||
|     <string name="download_finished_more">%s heruntergeladen</string> |     <string name="download_finished_more">%s heruntergeladen</string> | ||||||
|     <string name="generate_unique_name">Eindeutigen Namen erzeugen</string> |     <string name="generate_unique_name">Eindeutigen Namen erzeugen</string> | ||||||
|     <string name="overwrite">Überschreiben</string> |     <string name="overwrite">Überschreiben</string> | ||||||
|     <string name="overwrite_warning">Eine heruntergeladene Datei dieses Namens existiert bereits</string> |     <string name="overwrite_finished_warning">Eine heruntergeladene Datei dieses Namens existiert bereits</string> | ||||||
|     <string name="download_already_running">Eine Datei dieses Namens wird gerade heruntergeladen</string> |     <string name="download_already_running">Eine Datei dieses Namens wird gerade heruntergeladen</string> | ||||||
|     <string name="show_error">Fehler anzeigen</string> |     <string name="show_error">Fehler anzeigen</string> | ||||||
|     <string name="label_code">Code</string> |     <string name="label_code">Code</string> | ||||||
|     <string name="error_path_creation">Die Datei kann nicht erstellt werden</string> |     <string name="error_file_creation">Die Datei kann nicht erstellt werden</string> | ||||||
|     <string name="error_file_creation">Der Zielordner kann nicht erstellt werden</string> |     <string name="error_path_creation">Der Zielordner kann nicht erstellt werden</string> | ||||||
|     <string name="error_permission_denied">System verweigert den Zugriff</string> |     <string name="error_permission_denied">System verweigert den Zugriff</string> | ||||||
|     <string name="error_ssl_exception">Sichere Verbindung fehlgeschlagen</string> |     <string name="error_ssl_exception">Sichere Verbindung fehlgeschlagen</string> | ||||||
|     <string name="error_unknown_host">Der Server konnte nicht gefunden werden</string> |     <string name="error_unknown_host">Der Server konnte nicht gefunden werden</string> | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ | |||||||
|     <string name="share_dialog_title">Compartir con</string> |     <string name="share_dialog_title">Compartir con</string> | ||||||
|     <string name="choose_browser">Elegir navegador</string> |     <string name="choose_browser">Elegir navegador</string> | ||||||
|     <string name="screen_rotation">rotación</string> |     <string name="screen_rotation">rotación</string> | ||||||
|     <string name="download_path_title">Ruta de descarga de vídeo</string> |     <string name="download_path_title">Carpeta de descarga de vídeo</string> | ||||||
|     <string name="download_path_summary">Ruta para almacenar los vídeos descargados</string> |     <string name="download_path_summary">Ruta para almacenar los vídeos descargados</string> | ||||||
|     <string name="download_path_dialog_title">Introducir directorio de descargas para vídeos</string> |     <string name="download_path_dialog_title">Introducir directorio de descargas para vídeos</string> | ||||||
|     <string name="default_resolution_title">Resolución por defecto de vídeo</string> |     <string name="default_resolution_title">Resolución por defecto de vídeo</string> | ||||||
| @@ -40,7 +40,7 @@ | |||||||
|     <string name="use_tor_summary">(Experimental) Forzar la descarga a través de Tor para una mayor privacidad (transmisión de vídeos aún no compatible).</string> |     <string name="use_tor_summary">(Experimental) Forzar la descarga a través de Tor para una mayor privacidad (transmisión de vídeos aún no compatible).</string> | ||||||
|     <string name="err_dir_create">No se puede crear la carpeta de descarga \'%1$s\'</string> |     <string name="err_dir_create">No se puede crear la carpeta de descarga \'%1$s\'</string> | ||||||
|     <string name="info_dir_created">Carpeta de descarga creada \'%1$s\'</string> |     <string name="info_dir_created">Carpeta de descarga creada \'%1$s\'</string> | ||||||
|     <string name="download_path_audio_summary">Los audios descargados se almacenan aquí</string> |     <string name="download_path_audio_summary">Ruta para almacenar los audios descargados</string> | ||||||
|     <string name="download_path_audio_dialog_title">Introducir ruta de descarga para archivos de audio</string> |     <string name="download_path_audio_dialog_title">Introducir ruta de descarga para archivos de audio</string> | ||||||
|     <string name="blocked_by_gema">Bloqueado por GEMA</string> |     <string name="blocked_by_gema">Bloqueado por GEMA</string> | ||||||
|     <string name="download_path_audio_title">Carpeta de descarga de audio</string> |     <string name="download_path_audio_title">Carpeta de descarga de audio</string> | ||||||
| @@ -418,7 +418,9 @@ abrir en modo popup</string> | |||||||
|     <!-- dialog about existing downloads --> |     <!-- dialog about existing downloads --> | ||||||
|     <string name="generate_unique_name">Generar nombre único</string> |     <string name="generate_unique_name">Generar nombre único</string> | ||||||
|     <string name="overwrite">Sobrescribir</string> |     <string name="overwrite">Sobrescribir</string> | ||||||
|     <string name="overwrite_warning">Ya existe un archivo descargado con este nombre</string> |     <string name="overwrite_unrelated_warning">Ya existe un archivo con este nombre</string> | ||||||
|  |     <string name="overwrite_finished_warning">Ya existe un archivo descargado con este nombre</string> | ||||||
|  |     <string name="overwrite_failed">No se puede sobrescribir el archivo</string> | ||||||
|     <string name="download_already_running">Hay una descarga en curso con este nombre</string> |     <string name="download_already_running">Hay una descarga en curso con este nombre</string> | ||||||
|     <string name="download_already_pending">Hay una descarga pendiente con este nombre</string> |     <string name="download_already_pending">Hay una descarga pendiente con este nombre</string> | ||||||
|  |  | ||||||
| @@ -440,8 +442,8 @@ abrir en modo popup</string> | |||||||
|     <!-- message dialog about download error --> |     <!-- message dialog about download error --> | ||||||
|     <string name="show_error">Mostrar error</string> |     <string name="show_error">Mostrar error</string> | ||||||
|     <string name="label_code">Codigo</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 la carpeta de destino</string> | ||||||
|     <string name="error_file_creation">No se puede crear el archivo</string> |     <string name="error_path_creation">No se puede crear el archivo</string> | ||||||
|     <string name="error_permission_denied">Permiso denegado por el sistema</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_ssl_exception">Fallo la conexión segura</string> | ||||||
|     <string name="error_unknown_host">No se pudo encontrar el servidor</string> |     <string name="error_unknown_host">No se pudo encontrar el servidor</string> | ||||||
| @@ -453,6 +455,19 @@ abrir en modo popup</string> | |||||||
|     <string name="error_postprocessing_failed">Fallo el post-procesado</string> |     <string name="error_postprocessing_failed">Fallo el post-procesado</string> | ||||||
|     <string name="error_postprocessing_stopped">NewPipe se cerro mientras se trabajaba en el archivo</string> |     <string name="error_postprocessing_stopped">NewPipe se cerro mientras se trabajaba en el archivo</string> | ||||||
|     <string name="error_insufficient_storage">No hay suficiente espacio disponible en el dispositivo</string> |     <string name="error_insufficient_storage">No hay suficiente espacio disponible en el dispositivo</string> | ||||||
|  |     <string name="error_progress_lost">Se perdió el progreso porque el archivo fue eliminado</string> | ||||||
|  |  | ||||||
|  |     <string name="downloads_storage">API de almacenamiento</string> | ||||||
|  |     <string name="downloads_storage_desc">Seleccione que API utilizar para almacenar las descargas</string> | ||||||
|  |  | ||||||
|  |     <string name="storage_access_framework_description">Framework de acceso a almacenamiento</string> | ||||||
|  |     <string name="java_io_description">Java I/O</string> | ||||||
|  |  | ||||||
|  |     <string name="save_as">Guardar como…</string> | ||||||
|  |  | ||||||
|  |     <string name="download_to_sdcard_error_message">No es posible descargar a una tarjeta SD externa. \¿Restablecer la ubicación de la carpeta de descarga\?</string> | ||||||
|  |  | ||||||
|  |     <string name="download_pick_path">Seleccione los directorios de descarga</string> | ||||||
|  |  | ||||||
|     <string name="unsubscribe">Desuscribirse</string> |     <string name="unsubscribe">Desuscribirse</string> | ||||||
|     <string name="tab_new">Nueva pestaña</string> |     <string name="tab_new">Nueva pestaña</string> | ||||||
|   | |||||||
| @@ -446,12 +446,12 @@ | |||||||
|     <string name="download_finished_more">%s deskarga amaituta</string> |     <string name="download_finished_more">%s deskarga amaituta</string> | ||||||
|     <string name="generate_unique_name">Sortu izen bakana</string> |     <string name="generate_unique_name">Sortu izen bakana</string> | ||||||
|     <string name="overwrite">Gainidatzi</string> |     <string name="overwrite">Gainidatzi</string> | ||||||
|     <string name="overwrite_warning">Badago izen bera duen deskargatutako fitxategi bat</string> |     <string name="overwrite_finished_warning">Badago izen bera duen deskargatutako fitxategi bat</string> | ||||||
|     <string name="download_already_running">Badago izen bera duen deskarga bat abian</string> |     <string name="download_already_running">Badago izen bera duen deskarga bat abian</string> | ||||||
|     <string name="show_error">Erakutsi errorea</string> |     <string name="show_error">Erakutsi errorea</string> | ||||||
|     <string name="label_code">Kodea</string> |     <string name="label_code">Kodea</string> | ||||||
|     <string name="error_path_creation">Ezin da fitxategia sortu</string> |     <string name="error_file_creation">Ezin da fitxategia sortu</string> | ||||||
|     <string name="error_file_creation">Ezin da helburu karpeta sortu</string> |     <string name="error_path_creation">Ezin da helburu karpeta sortu</string> | ||||||
|     <string name="error_permission_denied">Sistemak baimena ukatu du</string> |     <string name="error_permission_denied">Sistemak baimena ukatu du</string> | ||||||
|     <string name="error_ssl_exception">Konexio seguruak huts egin du</string> |     <string name="error_ssl_exception">Konexio seguruak huts egin du</string> | ||||||
|     <string name="error_unknown_host">Ezin izan da zerbitzaria aurkitu</string> |     <string name="error_unknown_host">Ezin izan da zerbitzaria aurkitu</string> | ||||||
|   | |||||||
| @@ -451,12 +451,12 @@ | |||||||
|     <string name="download_finished_more">%s הורדות הסתיימו</string> |     <string name="download_finished_more">%s הורדות הסתיימו</string> | ||||||
|     <string name="generate_unique_name">יצירת שם ייחודי</string> |     <string name="generate_unique_name">יצירת שם ייחודי</string> | ||||||
|     <string name="overwrite">שכתוב</string> |     <string name="overwrite">שכתוב</string> | ||||||
|     <string name="overwrite_warning">כבר קיים קובץ בשם הזה</string> |     <string name="overwrite_finished_warning">כבר קיים קובץ בשם הזה</string> | ||||||
|     <string name="download_already_running">אחת ההורדות הפעילות כבר נושאת את השם הזה</string> |     <string name="download_already_running">אחת ההורדות הפעילות כבר נושאת את השם הזה</string> | ||||||
|     <string name="show_error">הצגת שגיאה</string> |     <string name="show_error">הצגת שגיאה</string> | ||||||
|     <string name="label_code">קוד</string> |     <string name="label_code">קוד</string> | ||||||
|     <string name="error_path_creation">לא ניתן ליצור את הקובץ</string> |     <string name="error_file_creation">לא ניתן ליצור את הקובץ</string> | ||||||
|     <string name="error_file_creation">לא ניתן ליצור את תיקיית היעד</string> |     <string name="error_path_creation">לא ניתן ליצור את תיקיית היעד</string> | ||||||
|     <string name="error_permission_denied">ההרשאה נדחתה על ידי המערכת</string> |     <string name="error_permission_denied">ההרשאה נדחתה על ידי המערכת</string> | ||||||
|     <string name="error_ssl_exception">החיבור המאובטח נכשל</string> |     <string name="error_ssl_exception">החיבור המאובטח נכשל</string> | ||||||
|     <string name="error_unknown_host">לא ניתן למצוא את השרת</string> |     <string name="error_unknown_host">לא ניתן למצוא את השרת</string> | ||||||
|   | |||||||
| @@ -447,12 +447,12 @@ | |||||||
|     <string name="download_finished_more">%s unduhan selesai</string> |     <string name="download_finished_more">%s unduhan selesai</string> | ||||||
|     <string name="generate_unique_name">Hasilkan nama unik</string> |     <string name="generate_unique_name">Hasilkan nama unik</string> | ||||||
|     <string name="overwrite">Timpa</string> |     <string name="overwrite">Timpa</string> | ||||||
|     <string name="overwrite_warning">File yang diunduh dengan nama ini sudah ada</string> |     <string name="overwrite_finished_warning">File yang diunduh dengan nama ini sudah ada</string> | ||||||
|     <string name="download_already_running">Ada unduhan yang sedang berlangsung dengan nama ini</string> |     <string name="download_already_running">Ada unduhan yang sedang berlangsung dengan nama ini</string> | ||||||
|     <string name="show_error">Tunjukkan kesalahan</string> |     <string name="show_error">Tunjukkan kesalahan</string> | ||||||
|     <string name="label_code">Kode</string> |     <string name="label_code">Kode</string> | ||||||
|     <string name="error_path_creation">File tidak dapat dibuat</string> |     <string name="error_file_creation">File tidak dapat dibuat</string> | ||||||
|     <string name="error_file_creation">Folder tujuan tidak dapat dibuat</string> |     <string name="error_path_creation">Folder tujuan tidak dapat dibuat</string> | ||||||
|     <string name="error_permission_denied">Izin ditolak oleh sistem</string> |     <string name="error_permission_denied">Izin ditolak oleh sistem</string> | ||||||
|     <string name="error_ssl_exception">Koneksi aman gagal</string> |     <string name="error_ssl_exception">Koneksi aman gagal</string> | ||||||
|     <string name="error_unknown_host">Tidak dapat menemukan server</string> |     <string name="error_unknown_host">Tidak dapat menemukan server</string> | ||||||
|   | |||||||
| @@ -449,12 +449,12 @@ | |||||||
|     <string name="download_finished_more">%s download finiti</string> |     <string name="download_finished_more">%s download finiti</string> | ||||||
|     <string name="generate_unique_name">Genera un nome unico</string> |     <string name="generate_unique_name">Genera un nome unico</string> | ||||||
|     <string name="overwrite">Sovrascrivi</string> |     <string name="overwrite">Sovrascrivi</string> | ||||||
|     <string name="overwrite_warning">Esiste già un file scaricato con lo stesso nome</string> |     <string name="overwrite_finished_warning">Esiste già un file scaricato con lo stesso nome</string> | ||||||
|     <string name="download_already_running">C\'è un download in progresso con questo nome</string> |     <string name="download_already_running">C\'è un download in progresso con questo nome</string> | ||||||
|     <string name="show_error">Mostra errore</string> |     <string name="show_error">Mostra errore</string> | ||||||
|     <string name="label_code">Codice</string> |     <string name="label_code">Codice</string> | ||||||
|     <string name="error_path_creation">Impossibile creare il file</string> |     <string name="error_file_creation">Impossibile creare il file</string> | ||||||
|     <string name="error_file_creation">Impossibile creare la cartella di destinazione</string> |     <string name="error_path_creation">Impossibile creare la cartella di destinazione</string> | ||||||
|     <string name="error_permission_denied">Permesso negato dal sistema</string> |     <string name="error_permission_denied">Permesso negato dal sistema</string> | ||||||
|     <string name="error_ssl_exception">Connessione sicura fallita</string> |     <string name="error_ssl_exception">Connessione sicura fallita</string> | ||||||
|     <string name="error_unknown_host">Impossibile trovare il server</string> |     <string name="error_unknown_host">Impossibile trovare il server</string> | ||||||
|   | |||||||
| @@ -443,12 +443,12 @@ | |||||||
|     <string name="download_finished_more">%s muat turun selesai</string> |     <string name="download_finished_more">%s muat turun selesai</string> | ||||||
|     <string name="generate_unique_name">Menjana nama yang unik</string> |     <string name="generate_unique_name">Menjana nama yang unik</string> | ||||||
|     <string name="overwrite">Timpa</string> |     <string name="overwrite">Timpa</string> | ||||||
|     <string name="overwrite_warning">Fail yang dimuat turun dengan nama ini sudah wujud</string> |     <string name="overwrite_finished_warning">Fail yang dimuat turun dengan nama ini sudah wujud</string> | ||||||
|     <string name="download_already_running">Terdapat muat turun yang sedang berjalan dengan nama ini</string> |     <string name="download_already_running">Terdapat muat turun yang sedang berjalan dengan nama ini</string> | ||||||
|     <string name="show_error">Tunjukkan kesilapan</string> |     <string name="show_error">Tunjukkan kesilapan</string> | ||||||
|     <string name="label_code">Kod</string> |     <string name="label_code">Kod</string> | ||||||
|     <string name="error_path_creation">Fail tidak boleh dibuat</string> |     <string name="error_file_creation">Fail tidak boleh dibuat</string> | ||||||
|     <string name="error_file_creation">Folder destinasi tidak boleh dibuat</string> |     <string name="error_path_creation">Folder destinasi tidak boleh dibuat</string> | ||||||
|     <string name="error_permission_denied">Kebenaran ditolak oleh sistem</string> |     <string name="error_permission_denied">Kebenaran ditolak oleh sistem</string> | ||||||
|     <string name="error_ssl_exception">Sambungan selamat gagal</string> |     <string name="error_ssl_exception">Sambungan selamat gagal</string> | ||||||
|     <string name="error_unknown_host">Tidak dapat mencari server</string> |     <string name="error_unknown_host">Tidak dapat mencari server</string> | ||||||
|   | |||||||
| @@ -526,12 +526,12 @@ | |||||||
|     <string name="download_finished_more">%s nedlastinger fullført</string> |     <string name="download_finished_more">%s nedlastinger fullført</string> | ||||||
|     <string name="generate_unique_name">Generer unikt navn</string> |     <string name="generate_unique_name">Generer unikt navn</string> | ||||||
|     <string name="overwrite">Overskriv</string> |     <string name="overwrite">Overskriv</string> | ||||||
|     <string name="overwrite_warning">Nedlastet fil ved dette navnet finnes allerede</string> |     <string name="overwrite_finished_warning">Nedlastet fil ved dette navnet finnes allerede</string> | ||||||
|     <string name="download_already_running">Nedlasting med dette navnet underveis allerede</string> |     <string name="download_already_running">Nedlasting med dette navnet underveis allerede</string> | ||||||
|     <string name="show_error">Vis feil</string> |     <string name="show_error">Vis feil</string> | ||||||
|     <string name="label_code">Kode</string> |     <string name="label_code">Kode</string> | ||||||
|     <string name="error_path_creation">Filen kan ikke opprettes</string> |     <string name="error_file_creation">Filen kan ikke opprettes</string> | ||||||
|     <string name="error_file_creation">Målmappen kan ikke opprettes</string> |     <string name="error_path_creation">Målmappen kan ikke opprettes</string> | ||||||
|     <string name="error_permission_denied">Tilgang nektet av systemet</string> |     <string name="error_permission_denied">Tilgang nektet av systemet</string> | ||||||
|     <string name="error_ssl_exception">Sikker tilkobling mislyktes</string> |     <string name="error_ssl_exception">Sikker tilkobling mislyktes</string> | ||||||
|     <string name="error_unknown_host">Fant ikke tjeneren</string> |     <string name="error_unknown_host">Fant ikke tjeneren</string> | ||||||
|   | |||||||
| @@ -445,12 +445,12 @@ | |||||||
|     <string name="download_finished_more">%s downloads voltooid</string> |     <string name="download_finished_more">%s downloads voltooid</string> | ||||||
|     <string name="generate_unique_name">Unieke naam genereren</string> |     <string name="generate_unique_name">Unieke naam genereren</string> | ||||||
|     <string name="overwrite">Overschrijven</string> |     <string name="overwrite">Overschrijven</string> | ||||||
|     <string name="overwrite_warning">Der bestaat al een gedownload bestand met deze naam</string> |     <string name="overwrite_finished_warning">Der bestaat al een gedownload bestand met deze naam</string> | ||||||
|     <string name="download_already_running">Der is al een download met deze naam bezig</string> |     <string name="download_already_running">Der is al een download met deze naam bezig</string> | ||||||
|     <string name="show_error">Foutmelding weergeven</string> |     <string name="show_error">Foutmelding weergeven</string> | ||||||
|     <string name="label_code">Code</string> |     <string name="label_code">Code</string> | ||||||
|     <string name="error_path_creation">Het bestand kan niet aangemaakt worden</string> |     <string name="error_file_creation">Het bestand kan niet aangemaakt worden</string> | ||||||
|     <string name="error_file_creation">De doelmap kan niet aangemaakt worden</string> |     <string name="error_path_creation">De doelmap kan niet aangemaakt worden</string> | ||||||
|     <string name="error_permission_denied">Toelating geweigerd door het systeem</string> |     <string name="error_permission_denied">Toelating geweigerd door het systeem</string> | ||||||
|     <string name="error_ssl_exception">Beveiligde verbinding is mislukt</string> |     <string name="error_ssl_exception">Beveiligde verbinding is mislukt</string> | ||||||
|     <string name="error_unknown_host">Kon de server niet vinden</string> |     <string name="error_unknown_host">Kon de server niet vinden</string> | ||||||
|   | |||||||
| @@ -449,12 +449,12 @@ | |||||||
|     <string name="download_finished_more">%s downloads voltooid</string> |     <string name="download_finished_more">%s downloads voltooid</string> | ||||||
|     <string name="generate_unique_name">Genereer een unieke naam</string> |     <string name="generate_unique_name">Genereer een unieke naam</string> | ||||||
|     <string name="overwrite">Overschrijven</string> |     <string name="overwrite">Overschrijven</string> | ||||||
|     <string name="overwrite_warning">Er bestaat al een gedownload bestand met deze naam</string> |     <string name="overwrite_finished_warning">Er bestaat al een gedownload bestand met deze naam</string> | ||||||
|     <string name="download_already_running">Er is een download aan de gang met deze naam</string> |     <string name="download_already_running">Er is een download aan de gang met deze naam</string> | ||||||
|     <string name="show_error">Toon foutmelding</string> |     <string name="show_error">Toon foutmelding</string> | ||||||
|     <string name="label_code">Code</string> |     <string name="label_code">Code</string> | ||||||
|     <string name="error_path_creation">Het bestand kan niet worden gemaakt</string> |     <string name="error_file_creation">Het bestand kan niet worden gemaakt</string> | ||||||
|     <string name="error_file_creation">De doelmap kan niet worden gemaakt</string> |     <string name="error_path_creation">De doelmap kan niet worden gemaakt</string> | ||||||
|     <string name="error_permission_denied">Toestemming door het systeem geweigerd</string> |     <string name="error_permission_denied">Toestemming door het systeem geweigerd</string> | ||||||
|     <string name="error_ssl_exception">Beveiligde connectie is mislukt</string> |     <string name="error_ssl_exception">Beveiligde connectie is mislukt</string> | ||||||
|     <string name="error_unknown_host">Kon de server niet vinden</string> |     <string name="error_unknown_host">Kon de server niet vinden</string> | ||||||
|   | |||||||
| @@ -446,12 +446,12 @@ | |||||||
|     <string name="download_finished_more">%s pobieranie zostało zakończone</string> |     <string name="download_finished_more">%s pobieranie zostało zakończone</string> | ||||||
|     <string name="generate_unique_name">Wygeneruj unikalną nazwę</string> |     <string name="generate_unique_name">Wygeneruj unikalną nazwę</string> | ||||||
|     <string name="overwrite">Zastąp</string> |     <string name="overwrite">Zastąp</string> | ||||||
|     <string name="overwrite_warning">Pobrany plik o tej nazwie już istnieje</string> |     <string name="overwrite_finished_warning">Pobrany plik o tej nazwie już istnieje</string> | ||||||
|     <string name="download_already_running">Trwa pobieranie z tą nazwą</string> |     <string name="download_already_running">Trwa pobieranie z tą nazwą</string> | ||||||
|     <string name="show_error">Pokaż błąd</string> |     <string name="show_error">Pokaż błąd</string> | ||||||
|     <string name="label_code">Kod</string> |     <string name="label_code">Kod</string> | ||||||
|     <string name="error_path_creation">Nie można utworzyć pliku</string> |     <string name="error_file_creation">Nie można utworzyć pliku</string> | ||||||
|     <string name="error_file_creation">Nie można utworzyć folderu docelowego</string> |     <string name="error_path_creation">Nie można utworzyć folderu docelowego</string> | ||||||
|     <string name="error_permission_denied">Odmowa dostępu do systemu</string> |     <string name="error_permission_denied">Odmowa dostępu do systemu</string> | ||||||
|     <string name="error_ssl_exception">Bezpieczne połączenie nie powiodło się</string> |     <string name="error_ssl_exception">Bezpieczne połączenie nie powiodło się</string> | ||||||
|     <string name="error_unknown_host">Nie można znaleźć serwera</string> |     <string name="error_unknown_host">Nie można znaleźć serwera</string> | ||||||
|   | |||||||
| @@ -446,12 +446,12 @@ abrir em modo popup</string> | |||||||
|     <string name="download_finished_more">%s downloads terminados</string> |     <string name="download_finished_more">%s downloads terminados</string> | ||||||
|     <string name="generate_unique_name">Gerar nome único</string> |     <string name="generate_unique_name">Gerar nome único</string> | ||||||
|     <string name="overwrite">"Sobrescrever "</string> |     <string name="overwrite">"Sobrescrever "</string> | ||||||
|     <string name="overwrite_warning">Um arquivo baixado com esse nome já existe</string> |     <string name="overwrite_finished_warning">Um arquivo baixado com esse nome já existe</string> | ||||||
|     <string name="download_already_running">Existe um download em progresso com esse nome</string> |     <string name="download_already_running">Existe um download em progresso com esse nome</string> | ||||||
|     <string name="show_error">Mostrar erro</string> |     <string name="show_error">Mostrar erro</string> | ||||||
|     <string name="label_code">Código</string> |     <string name="label_code">Código</string> | ||||||
|     <string name="error_path_creation">O arquivo não pode ser criado</string> |     <string name="error_file_creation">O arquivo não pode ser criado</string> | ||||||
|     <string name="error_file_creation">A pasta de destino não pode ser criada</string> |     <string name="error_path_creation">A pasta de destino não pode ser criada</string> | ||||||
|     <string name="error_permission_denied">Permissão negada pelo sistema</string> |     <string name="error_permission_denied">Permissão negada pelo sistema</string> | ||||||
|     <string name="error_ssl_exception">"Falha na conexão segura "</string> |     <string name="error_ssl_exception">"Falha na conexão segura "</string> | ||||||
|     <string name="error_unknown_host">Não foi possível encontrar o servidor</string> |     <string name="error_unknown_host">Não foi possível encontrar o servidor</string> | ||||||
|   | |||||||
| @@ -442,12 +442,12 @@ | |||||||
|     <string name="download_finished_more">%s descargas terminadas</string> |     <string name="download_finished_more">%s descargas terminadas</string> | ||||||
|     <string name="generate_unique_name">Gerar nome único</string> |     <string name="generate_unique_name">Gerar nome único</string> | ||||||
|     <string name="overwrite">Sobrescrever</string> |     <string name="overwrite">Sobrescrever</string> | ||||||
|     <string name="overwrite_warning">Um ficheiro descarregado com este nome já existe</string> |     <string name="overwrite_finished_warning">Um ficheiro descarregado com este nome já existe</string> | ||||||
|     <string name="download_already_running">Já existe uma descarga em curso com este nome</string> |     <string name="download_already_running">Já existe uma descarga em curso com este nome</string> | ||||||
|     <string name="show_error">Mostrar erro</string> |     <string name="show_error">Mostrar erro</string> | ||||||
|     <string name="label_code">Código</string> |     <string name="label_code">Código</string> | ||||||
|     <string name="error_path_creation">O ficheiro não pode ser criado</string> |     <string name="error_file_creation">O ficheiro não pode ser criado</string> | ||||||
|     <string name="error_file_creation">A pasta de destino não pode ser criada</string> |     <string name="error_path_creation">A pasta de destino não pode ser criada</string> | ||||||
|     <string name="error_permission_denied">Permissão negada pelo sistema</string> |     <string name="error_permission_denied">Permissão negada pelo sistema</string> | ||||||
|     <string name="error_ssl_exception">Ligação segura falhou</string> |     <string name="error_ssl_exception">Ligação segura falhou</string> | ||||||
|     <string name="error_unknown_host">Não foi possível encontrar o servidor</string> |     <string name="error_unknown_host">Não foi possível encontrar o servidor</string> | ||||||
|   | |||||||
| @@ -442,12 +442,12 @@ | |||||||
|     <string name="permission_denied">Действие запрещено системой</string> |     <string name="permission_denied">Действие запрещено системой</string> | ||||||
|     <string name="download_failed">Ошибка загрузки</string> |     <string name="download_failed">Ошибка загрузки</string> | ||||||
|     <string name="overwrite">Перезаписать</string> |     <string name="overwrite">Перезаписать</string> | ||||||
|     <string name="overwrite_warning">Файл с таким именем уже существует</string> |     <string name="overwrite_finished_warning">Файл с таким именем уже существует</string> | ||||||
|     <string name="download_already_running">Загрузка с таким именем уже выполняется</string> |     <string name="download_already_running">Загрузка с таким именем уже выполняется</string> | ||||||
|     <string name="show_error">Показать текст ошибки</string> |     <string name="show_error">Показать текст ошибки</string> | ||||||
|     <string name="label_code">Код</string> |     <string name="label_code">Код</string> | ||||||
|     <string name="error_path_creation">Файл не может быть создан</string> |     <string name="error_path_creation">Папка назначения не может быть создана</string> | ||||||
|     <string name="error_file_creation">Папка назначения не может быть создана</string> |     <string name="error_file_creation">Файл не может быть создан</string> | ||||||
|     <string name="error_permission_denied">Доступ запрещен системой</string> |     <string name="error_permission_denied">Доступ запрещен системой</string> | ||||||
|     <string name="error_unknown_host">Сервер не найден</string> |     <string name="error_unknown_host">Сервер не найден</string> | ||||||
|     <string name="error_http_unsupported_range">"Сервер не поддерживает многопотоковую  загрузку, попробуйте с @string/msg_threads = 1"</string> |     <string name="error_http_unsupported_range">"Сервер не поддерживает многопотоковую  загрузку, попробуйте с @string/msg_threads = 1"</string> | ||||||
|   | |||||||
| @@ -449,12 +449,12 @@ | |||||||
|     <string name="download_finished_more">%s indirme bitti</string> |     <string name="download_finished_more">%s indirme bitti</string> | ||||||
|     <string name="generate_unique_name">Benzersiz ad oluştur</string> |     <string name="generate_unique_name">Benzersiz ad oluştur</string> | ||||||
|     <string name="overwrite">Üzerine yaz</string> |     <string name="overwrite">Üzerine yaz</string> | ||||||
|     <string name="overwrite_warning">Bu ada sahip indirilen bir dosya zaten var</string> |     <string name="overwrite_finished_warning">Bu ada sahip indirilen bir dosya zaten var</string> | ||||||
|     <string name="download_already_running">Bu ad ile devam eden bir indirme var</string> |     <string name="download_already_running">Bu ad ile devam eden bir indirme var</string> | ||||||
|     <string name="show_error">Hatayı göster</string> |     <string name="show_error">Hatayı göster</string> | ||||||
|     <string name="label_code">Kod</string> |     <string name="label_code">Kod</string> | ||||||
|     <string name="error_path_creation">Dosya oluşturulamıyor</string> |     <string name="error_file_creation">Dosya oluşturulamıyor</string> | ||||||
|     <string name="error_file_creation">Hedef klasör oluşturulamıyor</string> |     <string name="error_path_creation">Hedef klasör oluşturulamıyor</string> | ||||||
|     <string name="error_permission_denied">İzin sistem tarafından reddedildi</string> |     <string name="error_permission_denied">İzin sistem tarafından reddedildi</string> | ||||||
|     <string name="error_ssl_exception">Güvenli bağlantı başarısız</string> |     <string name="error_ssl_exception">Güvenli bağlantı başarısız</string> | ||||||
|     <string name="error_unknown_host">Sunucu bulunamadı</string> |     <string name="error_unknown_host">Sunucu bulunamadı</string> | ||||||
|   | |||||||
| @@ -440,11 +440,11 @@ | |||||||
|     <string name="download_finished_more">%s tải về đã xong</string> |     <string name="download_finished_more">%s tải về đã xong</string> | ||||||
|     <string name="generate_unique_name">Tạo tên riêng biệt</string> |     <string name="generate_unique_name">Tạo tên riêng biệt</string> | ||||||
|     <string name="overwrite">Ghi đè</string> |     <string name="overwrite">Ghi đè</string> | ||||||
|     <string name="overwrite_warning">Có một tệp đã tải về trùng tên</string> |     <string name="overwrite_finished_warning">Có một tệp đã tải về trùng tên</string> | ||||||
|     <string name="download_already_running">Có một tệp trùng tên đang tải về</string> |     <string name="download_already_running">Có một tệp trùng tên đang tải về</string> | ||||||
|     <string name="show_error">Hiện lỗi</string> |     <string name="show_error">Hiện lỗi</string> | ||||||
|     <string name="error_path_creation">Không thể tạo tệp</string> |     <string name="error_file_creation">Không thể tạo tệp</string> | ||||||
|     <string name="error_file_creation">Không thể tạo thư mục đích</string> |     <string name="error_path_creation">Không thể tạo thư mục đích</string> | ||||||
|     <string name="error_permission_denied">Quyền bị từ chối bởi hệ thống</string> |     <string name="error_permission_denied">Quyền bị từ chối bởi hệ thống</string> | ||||||
|     <string name="error_ssl_exception">Không thể tạo kết nối an toàn</string> |     <string name="error_ssl_exception">Không thể tạo kết nối an toàn</string> | ||||||
|     <string name="error_unknown_host">Không thể tìm máy chủ</string> |     <string name="error_unknown_host">Không thể tìm máy chủ</string> | ||||||
|   | |||||||
| @@ -445,12 +445,12 @@ | |||||||
|     <string name="download_finished_more">%s 個下載已結束</string> |     <string name="download_finished_more">%s 個下載已結束</string> | ||||||
|     <string name="generate_unique_name">生成獨特的名稱</string> |     <string name="generate_unique_name">生成獨特的名稱</string> | ||||||
|     <string name="overwrite">覆寫</string> |     <string name="overwrite">覆寫</string> | ||||||
|     <string name="overwrite_warning">已有此名稱的已下載檔案</string> |     <string name="overwrite_finished_warning">已有此名稱的已下載檔案</string> | ||||||
|     <string name="download_already_running">已有此名稱的當案的下載正在進行</string> |     <string name="download_already_running">已有此名稱的當案的下載正在進行</string> | ||||||
|     <string name="show_error">顯示錯誤</string> |     <string name="show_error">顯示錯誤</string> | ||||||
|     <string name="label_code">代碼</string> |     <string name="label_code">代碼</string> | ||||||
|     <string name="error_path_creation">無法建立檔案</string> |     <string name="error_file_creation">無法建立檔案</string> | ||||||
|     <string name="error_file_creation">無法建立目的地資料夾</string> |     <string name="error_path_creation">無法建立目的地資料夾</string> | ||||||
|     <string name="error_permission_denied">被系統拒絕的權限</string> |     <string name="error_permission_denied">被系統拒絕的權限</string> | ||||||
|     <string name="error_ssl_exception">安全連線失敗</string> |     <string name="error_ssl_exception">安全連線失敗</string> | ||||||
|     <string name="error_unknown_host">找不到伺服器</string> |     <string name="error_unknown_host">找不到伺服器</string> | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ | |||||||
|     <string name="saved_tabs_key" translatable="false">saved_tabs_key</string> |     <string name="saved_tabs_key" translatable="false">saved_tabs_key</string> | ||||||
|  |  | ||||||
|     <!-- Key values --> |     <!-- Key values --> | ||||||
|     <string name="download_path_key" translatable="false">download_path</string> |     <string name="download_path_video_key" translatable="false">download_path</string> | ||||||
|     <string name="download_path_audio_key" translatable="false">download_path_audio</string> |     <string name="download_path_audio_key" translatable="false">download_path_audio</string> | ||||||
|  |  | ||||||
|     <string name="use_external_video_player_key" translatable="false">use_external_video_player</string> |     <string name="use_external_video_player_key" translatable="false">use_external_video_player</string> | ||||||
| @@ -160,6 +160,21 @@ | |||||||
|     <string name="clear_views_history_key" translatable="false">clear_play_history</string> |     <string name="clear_views_history_key" translatable="false">clear_play_history</string> | ||||||
|     <string name="clear_search_history_key" translatable="false">clear_search_history</string> |     <string name="clear_search_history_key" translatable="false">clear_search_history</string> | ||||||
|  |  | ||||||
|  |     <string name="downloads_storage_api" translatable="false">downloads_storage_api</string> | ||||||
|  |  | ||||||
|  |     <!-- WARNING: changing the default value will require update the code too  --> | ||||||
|  |     <string name="downloads_storage_api_default" translatable="false">javaIO</string> | ||||||
|  |  | ||||||
|  |     <string-array name="downloads_storage_api_values" translatable="false"> | ||||||
|  |         <item translatable="false">SAF</item> | ||||||
|  |         <item translatable="false">javaIO</item> | ||||||
|  |     </string-array> | ||||||
|  |  | ||||||
|  |     <string-array name="downloads_storage_api_description" translatable="true"> | ||||||
|  |         <item translatable="true">@string/storage_access_framework_description</item> | ||||||
|  |         <item translatable="true">@string/java_io_description</item> | ||||||
|  |     </string-array> | ||||||
|  |  | ||||||
|     <!-- FileName Downloads  --> |     <!-- FileName Downloads  --> | ||||||
|     <string name="settings_file_charset_key" translatable="false">file_rename</string> |     <string name="settings_file_charset_key" translatable="false">file_rename</string> | ||||||
|     <string name="settings_file_replacement_character_key" translatable="false">file_replacement_character</string> |     <string name="settings_file_replacement_character_key" translatable="false">file_replacement_character</string> | ||||||
|   | |||||||
| @@ -176,7 +176,7 @@ | |||||||
|     <!-- error strings --> |     <!-- error strings --> | ||||||
|     <string name="general_error">Error</string> |     <string name="general_error">Error</string> | ||||||
|     <string name="download_to_sdcard_error_title">External storage unavailable</string> |     <string name="download_to_sdcard_error_title">External storage unavailable</string> | ||||||
|     <string name="download_to_sdcard_error_message">Downloading to external SD card not yet possible. Reset download folder location\?</string> |     <string name="download_to_sdcard_error_message">Downloading to external SD card not possible. Reset download folder location\?</string> | ||||||
|     <string name="network_error">Network error</string> |     <string name="network_error">Network error</string> | ||||||
|     <string name="could_not_load_thumbnails">Could not load all thumbnails</string> |     <string name="could_not_load_thumbnails">Could not load all thumbnails</string> | ||||||
|     <string name="youtube_signature_decryption_error">Could not decrypt video URL signature</string> |     <string name="youtube_signature_decryption_error">Could not decrypt video URL signature</string> | ||||||
| @@ -512,15 +512,17 @@ | |||||||
|     <!-- dialog about existing downloads --> |     <!-- dialog about existing downloads --> | ||||||
|     <string name="generate_unique_name">Generate unique name</string> |     <string name="generate_unique_name">Generate unique name</string> | ||||||
|     <string name="overwrite">Overwrite</string> |     <string name="overwrite">Overwrite</string> | ||||||
|     <string name="overwrite_warning">A downloaded file with this name already exists</string> |     <string name="overwrite_unrelated_warning">A file with this name already exists</string> | ||||||
|  |     <string name="overwrite_finished_warning">A downloaded file with this name already exists</string> | ||||||
|  |     <string name="overwrite_failed">cannot overwrite the file</string> | ||||||
|     <string name="download_already_running">There is a download in progress with this name</string> |     <string name="download_already_running">There is a download in progress with this name</string> | ||||||
|     <string name="download_already_pending">There is a pending download with this name</string> |     <string name="download_already_pending">There is a pending download with this name</string> | ||||||
|  |  | ||||||
|     <!-- message dialog about download error --> |     <!-- message dialog about download error --> | ||||||
|     <string name="show_error">Show error</string> |     <string name="show_error">Show error</string> | ||||||
|     <string name="label_code">Code</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 file can not be created</string> | ||||||
|     <string name="error_file_creation">The destination folder can not be created</string> |     <string name="error_path_creation">The destination folder can not be created</string> | ||||||
|     <string name="error_permission_denied">Permission denied by the system</string> |     <string name="error_permission_denied">Permission denied by the system</string> | ||||||
|     <string name="error_ssl_exception">Secure connection failed</string> |     <string name="error_ssl_exception">Secure connection failed</string> | ||||||
|     <string name="error_unknown_host">Could not find the server</string> |     <string name="error_unknown_host">Could not find the server</string> | ||||||
| @@ -532,6 +534,7 @@ | |||||||
|     <string name="error_postprocessing_failed">Post-processing failed</string> |     <string name="error_postprocessing_failed">Post-processing failed</string> | ||||||
|     <string name="error_postprocessing_stopped">NewPipe was closed while working on the file</string> |     <string name="error_postprocessing_stopped">NewPipe was closed while working on the file</string> | ||||||
|     <string name="error_insufficient_storage">No space left on device</string> |     <string name="error_insufficient_storage">No space left on device</string> | ||||||
|  |     <string name="error_progress_lost">Progress lost, because the file was deleted</string> | ||||||
|  |  | ||||||
|     <string name="clear_finished_download">Clear finished downloads</string> |     <string name="clear_finished_download">Clear finished downloads</string> | ||||||
|     <string name="msg_pending_downloads">Continue your %s pending transfers from Downloads</string> |     <string name="msg_pending_downloads">Continue your %s pending transfers from Downloads</string> | ||||||
| @@ -546,4 +549,14 @@ | |||||||
|     <string name="start_downloads">Start downloads</string> |     <string name="start_downloads">Start downloads</string> | ||||||
|     <string name="pause_downloads">Pause downloads</string> |     <string name="pause_downloads">Pause downloads</string> | ||||||
|  |  | ||||||
|  |     <string name="downloads_storage">Storage API</string> | ||||||
|  |     <string name="downloads_storage_desc">Select which API use to store the downloads</string> | ||||||
|  |  | ||||||
|  |     <string name="storage_access_framework_description">Storage Access Framework</string> | ||||||
|  |     <string name="java_io_description">Java I/O</string> | ||||||
|  |  | ||||||
|  |     <string name="save_as">Save as…</string> | ||||||
|  |  | ||||||
|  |     <string name="download_pick_path">Select the downloads save path</string> | ||||||
|  |  | ||||||
| </resources> | </resources> | ||||||
| @@ -4,10 +4,26 @@ | |||||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|     android:title="@string/settings_category_downloads_title"> |     android:title="@string/settings_category_downloads_title"> | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     <Preference | ||||||
|  |         app:iconSpaceReserved="false" | ||||||
|  |         android:key="saf_test" | ||||||
|  |         android:summary="Realiza una prueba del Storage Access Framework de Android" | ||||||
|  |         android:title="Probar SAF"/> | ||||||
|  |  | ||||||
|  |     <ListPreference | ||||||
|  |         app:iconSpaceReserved="false" | ||||||
|  |         android:defaultValue="@string/downloads_storage_api_default" | ||||||
|  |         android:entries="@array/downloads_storage_api_description" | ||||||
|  |         android:entryValues="@array/downloads_storage_api_values" | ||||||
|  |         android:key="@string/downloads_storage_api" | ||||||
|  |         android:summary="@string/downloads_storage_desc" | ||||||
|  |         android:title="@string/downloads_storage" /> | ||||||
|  |  | ||||||
|     <Preference |     <Preference | ||||||
|         app:iconSpaceReserved="false" |         app:iconSpaceReserved="false" | ||||||
|         android:dialogTitle="@string/download_path_dialog_title" |         android:dialogTitle="@string/download_path_dialog_title" | ||||||
|         android:key="@string/download_path_key" |         android:key="@string/download_path_video_key" | ||||||
|         android:summary="@string/download_path_summary" |         android:summary="@string/download_path_summary" | ||||||
|         android:title="@string/download_path_title"/> |         android:title="@string/download_path_title"/> | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 kapodamy
					kapodamy