Merge branch 'dev' into removeextra
11
README.md
@ -66,15 +66,22 @@ NewPipe does not use any Google framework libraries, nor the YouTube API. Websit
|
|||||||
* Enqueue videos
|
* Enqueue videos
|
||||||
* Local playlists
|
* Local playlists
|
||||||
* Subtitles
|
* Subtitles
|
||||||
* Multi-service support (e.g. SoundCloud \[beta\])
|
|
||||||
* Livestream support
|
* Livestream support
|
||||||
|
* Show comments
|
||||||
|
|
||||||
### Coming Features
|
### Coming Features
|
||||||
|
|
||||||
* Cast to UPnP and Cast
|
* Cast to UPnP and Cast
|
||||||
* Show comments
|
|
||||||
* … and many more
|
* … and many more
|
||||||
|
|
||||||
|
### Supported Services
|
||||||
|
|
||||||
|
NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/documentation/) provide more info on how a new service can be added to the app and the extractor. Please get in touch with us if you intend to add a new one. Currently supported services are:
|
||||||
|
|
||||||
|
* YouTube
|
||||||
|
* SoundCloud \[beta\]
|
||||||
|
* media.ccc.de \[beta\]
|
||||||
|
|
||||||
## Updates
|
## Updates
|
||||||
When a change to the NewPipe code occurs (due to either adding features or bug fixing), eventually a release will occur. These are in the format x.xx.x . In order to get this new version, you can:
|
When a change to the NewPipe code occurs (due to either adding features or bug fixing), eventually a release will occur. These are in the format x.xx.x . In order to get this new version, you can:
|
||||||
* Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods.
|
* Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods.
|
||||||
|
@ -8,8 +8,8 @@ android {
|
|||||||
applicationId "org.schabi.newpipe"
|
applicationId "org.schabi.newpipe"
|
||||||
minSdkVersion 19
|
minSdkVersion 19
|
||||||
targetSdkVersion 28
|
targetSdkVersion 28
|
||||||
versionCode 720
|
versionCode 740
|
||||||
versionName "0.16.0"
|
versionName "0.16.2"
|
||||||
|
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
@ -57,7 +57,7 @@ dependencies {
|
|||||||
exclude module: 'support-annotations'
|
exclude module: 'support-annotations'
|
||||||
})
|
})
|
||||||
|
|
||||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:aa4f03a'
|
implementation 'com.github.TeamNewPipe:NewPipeExtractor:c64c90a'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
testImplementation 'org.mockito:mockito-core:2.23.0'
|
testImplementation 'org.mockito:mockito-core:2.23.0'
|
||||||
|
@ -574,7 +574,7 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
playQueue = new SinglePlayQueue((StreamInfo) info);
|
playQueue = new SinglePlayQueue((StreamInfo) info);
|
||||||
|
|
||||||
if (playerChoice.equals(videoPlayerKey)) {
|
if (playerChoice.equals(videoPlayerKey)) {
|
||||||
NavigationHelper.playOnMainPlayer(this, playQueue);
|
NavigationHelper.playOnMainPlayer(this, playQueue, true);
|
||||||
} else if (playerChoice.equals(backgroundPlayerKey)) {
|
} else if (playerChoice.equals(backgroundPlayerKey)) {
|
||||||
NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true);
|
NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true);
|
||||||
} else if (playerChoice.equals(popupPlayerKey)) {
|
} else if (playerChoice.equals(popupPlayerKey)) {
|
||||||
@ -587,11 +587,11 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info);
|
playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info);
|
||||||
|
|
||||||
if (playerChoice.equals(videoPlayerKey)) {
|
if (playerChoice.equals(videoPlayerKey)) {
|
||||||
NavigationHelper.playOnMainPlayer(this, playQueue);
|
NavigationHelper.playOnMainPlayer(this, playQueue, true);
|
||||||
} else if (playerChoice.equals(backgroundPlayerKey)) {
|
} else if (playerChoice.equals(backgroundPlayerKey)) {
|
||||||
NavigationHelper.playOnBackgroundPlayer(this, playQueue);
|
NavigationHelper.playOnBackgroundPlayer(this, playQueue, true);
|
||||||
} else if (playerChoice.equals(popupPlayerKey)) {
|
} else if (playerChoice.equals(popupPlayerKey)) {
|
||||||
NavigationHelper.playOnPopupPlayer(this, playQueue);
|
NavigationHelper.playOnPopupPlayer(this, playQueue, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -50,6 +50,11 @@ public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity
|
|||||||
" ORDER BY " + STREAM_ACCESS_DATE + " DESC")
|
" ORDER BY " + STREAM_ACCESS_DATE + " DESC")
|
||||||
public abstract Flowable<List<StreamHistoryEntry>> getHistory();
|
public abstract Flowable<List<StreamHistoryEntry>> getHistory();
|
||||||
|
|
||||||
|
@Query("SELECT * FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID +
|
||||||
|
" = :streamId ORDER BY " + STREAM_ACCESS_DATE + " DESC LIMIT 1")
|
||||||
|
@Nullable
|
||||||
|
public abstract StreamHistoryEntity getLatestEntry(final long streamId);
|
||||||
|
|
||||||
@Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
@Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||||
public abstract int deleteStreamHistory(final long streamId);
|
public abstract int deleteStreamHistory(final long streamId);
|
||||||
|
|
||||||
|
@ -4,6 +4,9 @@ package org.schabi.newpipe.database.stream.model;
|
|||||||
import android.arch.persistence.room.ColumnInfo;
|
import android.arch.persistence.room.ColumnInfo;
|
||||||
import android.arch.persistence.room.Entity;
|
import android.arch.persistence.room.Entity;
|
||||||
import android.arch.persistence.room.ForeignKey;
|
import android.arch.persistence.room.ForeignKey;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import static android.arch.persistence.room.ForeignKey.CASCADE;
|
import static android.arch.persistence.room.ForeignKey.CASCADE;
|
||||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
|
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
|
||||||
@ -22,6 +25,12 @@ public class StreamStateEntity {
|
|||||||
final public static String JOIN_STREAM_ID = "stream_id";
|
final public static String JOIN_STREAM_ID = "stream_id";
|
||||||
final public static String STREAM_PROGRESS_TIME = "progress_time";
|
final public static String STREAM_PROGRESS_TIME = "progress_time";
|
||||||
|
|
||||||
|
|
||||||
|
/** Playback state will not be saved, if playback time less than this threshold */
|
||||||
|
private static final int PLAYBACK_SAVE_THRESHOLD_START_SECONDS = 5;
|
||||||
|
/** Playback state will not be saved, if time left less than this threshold */
|
||||||
|
private static final int PLAYBACK_SAVE_THRESHOLD_END_SECONDS = 10;
|
||||||
|
|
||||||
@ColumnInfo(name = JOIN_STREAM_ID)
|
@ColumnInfo(name = JOIN_STREAM_ID)
|
||||||
private long streamUid;
|
private long streamUid;
|
||||||
|
|
||||||
@ -48,4 +57,18 @@ public class StreamStateEntity {
|
|||||||
public void setProgressTime(long progressTime) {
|
public void setProgressTime(long progressTime) {
|
||||||
this.progressTime = progressTime;
|
this.progressTime = progressTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isValid(int durationInSeconds) {
|
||||||
|
final int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(progressTime);
|
||||||
|
return seconds > PLAYBACK_SAVE_THRESHOLD_START_SECONDS
|
||||||
|
&& seconds < durationInSeconds - PLAYBACK_SAVE_THRESHOLD_END_SECONDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(@Nullable Object obj) {
|
||||||
|
if (obj instanceof StreamStateEntity) {
|
||||||
|
return ((StreamStateEntity) obj).streamUid == streamUid
|
||||||
|
&& ((StreamStateEntity) obj).progressTime == progressTime;
|
||||||
|
} else return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,14 +1,23 @@
|
|||||||
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;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.annotation.StringRes;
|
||||||
import android.support.v4.app.DialogFragment;
|
import android.support.v4.app.DialogFragment;
|
||||||
|
import android.support.v4.provider.DocumentFile;
|
||||||
import android.support.v7.app.AlertDialog;
|
import android.support.v7.app.AlertDialog;
|
||||||
|
import android.support.v7.view.menu.ActionMenuItemView;
|
||||||
import android.support.v7.widget.Toolbar;
|
import android.support.v7.widget.Toolbar;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
@ -34,7 +43,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;
|
||||||
@ -43,19 +53,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.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;
|
||||||
@ -80,7 +98,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;
|
||||||
|
|
||||||
@ -160,7 +178,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(getContext()));
|
context = getContext();
|
||||||
|
|
||||||
|
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);
|
||||||
@ -177,9 +197,59 @@ 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();
|
||||||
|
askForSavePath = mgr.askForSavePath();
|
||||||
|
|
||||||
|
okButton.setEnabled(true);
|
||||||
|
|
||||||
|
context.unbindService(this);
|
||||||
|
|
||||||
|
// check of download paths are defined
|
||||||
|
if (!askForSavePath) {
|
||||||
|
String msg = "";
|
||||||
|
if (mainStorageVideo == null) msg += getString(R.string.download_path_title);
|
||||||
|
if (mainStorageAudio == null)
|
||||||
|
msg += getString(R.string.download_path_audio_title);
|
||||||
|
|
||||||
|
if (!msg.isEmpty()) {
|
||||||
|
String title;
|
||||||
|
if (mainStorageVideo == null && mainStorageAudio == null) {
|
||||||
|
title = getString(R.string.general_error);
|
||||||
|
msg = getString(R.string.no_available_dir) + ":\n" + msg;
|
||||||
|
} else {
|
||||||
|
title = msg;
|
||||||
|
msg = getString(R.string.no_available_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
new AlertDialog.Builder(context)
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.setTitle(title)
|
||||||
|
.setMessage(msg)
|
||||||
|
.create()
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceDisconnected(ComponentName name) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
}, Context.BIND_AUTO_CREATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -204,8 +274,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();
|
||||||
@ -240,17 +310,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();
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@ -263,22 +333,49 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(Bundle outState) {
|
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentFile docFile = DocumentFile.fromSingleUri(context, data.getData());
|
||||||
|
if (docFile == null) {
|
||||||
|
showFailedDialog(R.string.general_error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the selected file was previously used
|
||||||
|
checkSelectedDownload(null, data.getData(), docFile.getName(), docFile.getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// 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());
|
||||||
|
|
||||||
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());
|
||||||
|
|
||||||
|
okButton = toolbar.findViewById(R.id.okay);
|
||||||
|
okButton.setEnabled(false);// disable until the download service connection is done
|
||||||
|
|
||||||
toolbar.setOnMenuItemClickListener(item -> {
|
toolbar.setOnMenuItemClickListener(item -> {
|
||||||
if (item.getItemId() == R.id.okay) {
|
if (item.getItemId() == R.id.okay) {
|
||||||
prepareSelectedDownload();
|
prepareSelectedDownload();
|
||||||
@ -346,7 +443,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;
|
||||||
@ -370,9 +467,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;
|
||||||
@ -397,9 +494,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) {
|
||||||
@ -434,98 +531,297 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StoredDirectoryHelper mainStorageAudio = null;
|
||||||
|
StoredDirectoryHelper mainStorageVideo = null;
|
||||||
|
DownloadManager downloadManager = null;
|
||||||
|
ActionMenuItemView okButton = null;
|
||||||
|
Context context;
|
||||||
|
boolean askForSavePath;
|
||||||
|
|
||||||
|
private String getNameEditText() {
|
||||||
|
String str = nameEditText.getText().toString().trim();
|
||||||
|
|
||||||
|
return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showFailedDialog(@StringRes int msg) {
|
||||||
|
new AlertDialog.Builder(context)
|
||||||
|
.setTitle(R.string.general_error)
|
||||||
|
.setMessage(msg)
|
||||||
|
.setNegativeButton(android.R.string.ok, null)
|
||||||
|
.create()
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showErrorActivity(Exception e) {
|
||||||
|
ErrorActivity.reportError(
|
||||||
|
context,
|
||||||
|
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();
|
StoredDirectoryHelper mainStorage;
|
||||||
Stream stream;
|
MediaFormat format;
|
||||||
String location;
|
String mime;
|
||||||
char kind;
|
|
||||||
|
|
||||||
String fileName = nameEditText.getText().toString().trim();
|
// first, build the filename and get the output folder (if possible)
|
||||||
if (fileName.isEmpty())
|
// later, run a very very very large file checking logic
|
||||||
fileName = FilenameUtils.createFilename(context, currentInfo.getName());
|
|
||||||
|
|
||||||
switch (radioVideoAudioGroup.getCheckedRadioButtonId()) {
|
String filename = getNameEditText().concat(".");
|
||||||
|
|
||||||
|
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;
|
||||||
|
default:
|
||||||
|
throw new RuntimeException("No stream selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mainStorage == null || askForSavePath) {
|
||||||
|
// This part is called if with SAF preferred:
|
||||||
|
// * older android version running
|
||||||
|
// * save path not defined (via download settings)
|
||||||
|
// * the user as checked the "ask where to download" option
|
||||||
|
|
||||||
|
StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_PATH_SAF, filename, mime);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for existing file with the same name
|
||||||
|
checkSelectedDownload(mainStorage, mainStorage.findFile(filename), filename, mime);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkSelectedDownload(StoredDirectoryHelper mainStorage, Uri targetFile, String filename, String mime) {
|
||||||
|
StoredFileHelper storage;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (mainStorage == null) {
|
||||||
|
// using SAF on older android version
|
||||||
|
storage = new StoredFileHelper(context, null, targetFile, "");
|
||||||
|
} else if (targetFile == null) {
|
||||||
|
// the file does not exist, but it is probably used in a pending download
|
||||||
|
storage = new StoredFileHelper(mainStorage.getUri(), filename, mime, mainStorage.getTag());
|
||||||
|
} else {
|
||||||
|
// the target filename is already use, attempt to use it
|
||||||
|
storage = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
showErrorActivity(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if is our file
|
||||||
|
MissionState state = downloadManager.checkForExistingMission(storage);
|
||||||
|
@StringRes int msgBtn;
|
||||||
|
@StringRes int msgBody;
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case Finished:
|
||||||
|
msgBtn = R.string.overwrite;
|
||||||
|
msgBody = R.string.overwrite_finished_warning;
|
||||||
|
break;
|
||||||
|
case Pending:
|
||||||
|
msgBtn = R.string.overwrite;
|
||||||
|
msgBody = R.string.download_already_pending;
|
||||||
|
break;
|
||||||
|
case PendingRunning:
|
||||||
|
msgBtn = R.string.generate_unique_name;
|
||||||
|
msgBody = R.string.download_already_running;
|
||||||
|
break;
|
||||||
|
case None:
|
||||||
|
if (mainStorage == null) {
|
||||||
|
// This part is called if:
|
||||||
|
// * using SAF on older android version
|
||||||
|
// * save path not defined
|
||||||
|
continueSelectedDownload(storage);
|
||||||
|
return;
|
||||||
|
} else if (targetFile == null) {
|
||||||
|
// This part is called if:
|
||||||
|
// * the filename is not used in a pending/finished download
|
||||||
|
// * the file does not exists, create
|
||||||
|
|
||||||
|
if (!mainStorage.mkdirs()) {
|
||||||
|
showFailedDialog(R.string.error_path_creation);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
storage = mainStorage.createFile(filename, mime);
|
||||||
|
if (storage == null || !storage.canWrite()) {
|
||||||
|
showFailedDialog(R.string.error_file_creation);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
continueSelectedDownload(storage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
msgBtn = R.string.overwrite;
|
||||||
|
msgBody = R.string.overwrite_unrelated_warning;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int threads;
|
|
||||||
|
|
||||||
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
|
AlertDialog.Builder askDialog = new AlertDialog.Builder(context)
|
||||||
threads = 1;// use unique thread for subtitles due small file size
|
.setTitle(R.string.download_dialog_title)
|
||||||
fileName += ".srt";// final subtitle format
|
.setMessage(msgBody)
|
||||||
} else {
|
.setNegativeButton(android.R.string.cancel, null);
|
||||||
threads = threadsSeekBar.getProgress() + 1;
|
final StoredFileHelper finalStorage = storage;
|
||||||
fileName += "." + stream.getFormat().getSuffix();
|
|
||||||
|
|
||||||
|
if (mainStorage == null) {
|
||||||
|
// This part is called if:
|
||||||
|
// * using SAF on older android version
|
||||||
|
// * save path not defined
|
||||||
|
switch (state) {
|
||||||
|
case Pending:
|
||||||
|
case Finished:
|
||||||
|
askDialog.setPositiveButton(msgBtn, (dialog, which) -> {
|
||||||
|
dialog.dismiss();
|
||||||
|
downloadManager.forgetMission(finalStorage);
|
||||||
|
continueSelectedDownload(finalStorage);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
askDialog.create().show();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final String finalFileName = fileName;
|
askDialog.setPositiveButton(msgBtn, (dialog, which) -> {
|
||||||
|
dialog.dismiss();
|
||||||
|
|
||||||
DownloadManagerService.checkForRunningMission(context, location, fileName, (listed, finished) -> {
|
StoredFileHelper storageNew;
|
||||||
if (listed) {
|
switch (state) {
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
case Finished:
|
||||||
builder.setTitle(R.string.download_dialog_title)
|
case Pending:
|
||||||
.setMessage(finished ? R.string.overwrite_warning : R.string.download_already_running)
|
downloadManager.forgetMission(finalStorage);
|
||||||
.setPositiveButton(
|
case None:
|
||||||
finished ? R.string.overwrite : R.string.generate_unique_name,
|
if (targetFile == null) {
|
||||||
(dialog, which) -> downloadSelected(context, stream, location, finalFileName, kind, threads)
|
storageNew = mainStorage.createFile(filename, mime);
|
||||||
)
|
} else {
|
||||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
|
try {
|
||||||
dialog.cancel();
|
// try take (or steal) the file
|
||||||
})
|
storageNew = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag());
|
||||||
.create()
|
} catch (IOException e) {
|
||||||
.show();
|
Log.e(TAG, "Failed to take (or steal) the file in " + targetFile.toString());
|
||||||
} else {
|
storageNew = null;
|
||||||
downloadSelected(context, stream, location, finalFileName, kind, threads);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storageNew != null && storageNew.canWrite())
|
||||||
|
continueSelectedDownload(storageNew);
|
||||||
|
else
|
||||||
|
showFailedDialog(R.string.error_file_creation);
|
||||||
|
break;
|
||||||
|
case PendingRunning:
|
||||||
|
storageNew = mainStorage.createUniqueFile(filename, mime);
|
||||||
|
if (storageNew == null)
|
||||||
|
showFailedDialog(R.string.error_file_creation);
|
||||||
|
else
|
||||||
|
continueSelectedDownload(storageNew);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
askDialog.create().show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void downloadSelected(Context context, Stream selectedStream, String location, String fileName, char kind, int threads) {
|
private void continueSelectedDownload(@NonNull StoredFileHelper storage) {
|
||||||
|
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);
|
||||||
|
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 VideoStream) {
|
// more download logic: select muxer, subtitle converter, etc.
|
||||||
SecondaryStreamHelper<AudioStream> secondaryStream = videoStreamsAdapter
|
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
|
||||||
.getAllSecondary()
|
case R.id.audio_button:
|
||||||
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
|
threads = 1;// use unique thread for subtitles due small file size
|
||||||
|
kind = 'a';
|
||||||
|
selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex);
|
||||||
|
|
||||||
if (secondaryStream != null) {
|
if (selectedStream.getFormat() == MediaFormat.M4A) {
|
||||||
secondaryStreamUrl = secondaryStream.getStream().getUrl();
|
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
|
||||||
psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER;
|
|
||||||
psArgs = null;
|
|
||||||
long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream);
|
|
||||||
|
|
||||||
// set nearLength, only, if both sizes are fetched or known. this probably does not work on slow networks
|
|
||||||
if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) {
|
|
||||||
nearLength = secondaryStream.getSizeInBytes() + videoSize;
|
|
||||||
}
|
}
|
||||||
}
|
break;
|
||||||
} else if ((selectedStream instanceof SubtitlesStream) && selectedStream.getFormat() == MediaFormat.TTML) {
|
case R.id.video_button:
|
||||||
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
|
kind = 'v';
|
||||||
psArgs = new String[]{
|
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
|
||||||
selectedStream.getFormat().getSuffix(),
|
|
||||||
"false",// ignore empty frames
|
SecondaryStreamHelper<AudioStream> secondaryStream = videoStreamsAdapter
|
||||||
"false",// detect youtube duplicate lines
|
.getAllSecondary()
|
||||||
};
|
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
|
||||||
|
|
||||||
|
if (secondaryStream != null) {
|
||||||
|
secondaryStreamUrl = secondaryStream.getStream().getUrl();
|
||||||
|
|
||||||
|
if (selectedStream.getFormat() == MediaFormat.MPEG_4)
|
||||||
|
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
|
||||||
|
else
|
||||||
|
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
|
||||||
|
|
||||||
|
psArgs = null;
|
||||||
|
long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream);
|
||||||
|
|
||||||
|
// set nearLength, only, if both sizes are fetched or known. This probably
|
||||||
|
// does not work on slow networks but is later updated in the downloader
|
||||||
|
if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) {
|
||||||
|
nearLength = secondaryStream.getSizeInBytes() + videoSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case R.id.subtitle_button:
|
||||||
|
kind = 's';
|
||||||
|
selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
|
||||||
|
|
||||||
|
if (selectedStream.getFormat() == MediaFormat.TTML) {
|
||||||
|
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
|
||||||
|
psArgs = new String[]{
|
||||||
|
selectedStream.getFormat().getSuffix(),
|
||||||
|
"false",// ignore empty frames
|
||||||
|
"false",// detect youtube duplicate lines
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (secondaryStreamUrl == null) {
|
if (secondaryStreamUrl == null) {
|
||||||
@ -534,8 +830,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,12 +89,14 @@ import org.schabi.newpipe.util.PermissionHelper;
|
|||||||
import org.schabi.newpipe.util.ShareUtils;
|
import org.schabi.newpipe.util.ShareUtils;
|
||||||
import org.schabi.newpipe.util.StreamItemAdapter;
|
import org.schabi.newpipe.util.StreamItemAdapter;
|
||||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
||||||
|
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
@ -118,7 +120,7 @@ public class VideoDetailFragment
|
|||||||
private static final int RELATED_STREAMS_UPDATE_FLAG = 0x1;
|
private static final int RELATED_STREAMS_UPDATE_FLAG = 0x1;
|
||||||
private static final int RESOLUTIONS_MENU_UPDATE_FLAG = 0x2;
|
private static final int RESOLUTIONS_MENU_UPDATE_FLAG = 0x2;
|
||||||
private static final int TOOLBAR_ITEMS_UPDATE_FLAG = 0x4;
|
private static final int TOOLBAR_ITEMS_UPDATE_FLAG = 0x4;
|
||||||
private static final int COMMENTS_UPDATE_FLAG = 0x4;
|
private static final int COMMENTS_UPDATE_FLAG = 0x8;
|
||||||
|
|
||||||
private boolean autoPlayEnabled;
|
private boolean autoPlayEnabled;
|
||||||
private boolean showRelatedStreams;
|
private boolean showRelatedStreams;
|
||||||
@ -136,6 +138,8 @@ public class VideoDetailFragment
|
|||||||
private Disposable currentWorker;
|
private Disposable currentWorker;
|
||||||
@NonNull
|
@NonNull
|
||||||
private CompositeDisposable disposables = new CompositeDisposable();
|
private CompositeDisposable disposables = new CompositeDisposable();
|
||||||
|
@Nullable
|
||||||
|
private Disposable positionSubscriber = null;
|
||||||
|
|
||||||
private List<VideoStream> sortedVideoStreams;
|
private List<VideoStream> sortedVideoStreams;
|
||||||
private int selectedVideoStreamIndex = -1;
|
private int selectedVideoStreamIndex = -1;
|
||||||
@ -153,6 +157,7 @@ public class VideoDetailFragment
|
|||||||
private View thumbnailBackgroundButton;
|
private View thumbnailBackgroundButton;
|
||||||
private ImageView thumbnailImageView;
|
private ImageView thumbnailImageView;
|
||||||
private ImageView thumbnailPlayButton;
|
private ImageView thumbnailPlayButton;
|
||||||
|
private AnimatedProgressBar positionView;
|
||||||
|
|
||||||
private View videoTitleRoot;
|
private View videoTitleRoot;
|
||||||
private TextView videoTitleTextView;
|
private TextView videoTitleTextView;
|
||||||
@ -165,6 +170,7 @@ public class VideoDetailFragment
|
|||||||
private TextView detailControlsDownload;
|
private TextView detailControlsDownload;
|
||||||
private TextView appendControlsDetail;
|
private TextView appendControlsDetail;
|
||||||
private TextView detailDurationView;
|
private TextView detailDurationView;
|
||||||
|
private TextView detailPositionView;
|
||||||
|
|
||||||
private LinearLayout videoDescriptionRootLayout;
|
private LinearLayout videoDescriptionRootLayout;
|
||||||
private TextView videoUploadDateView;
|
private TextView videoUploadDateView;
|
||||||
@ -259,6 +265,8 @@ public class VideoDetailFragment
|
|||||||
// Check if it was loading when the fragment was stopped/paused,
|
// Check if it was loading when the fragment was stopped/paused,
|
||||||
if (wasLoading.getAndSet(false)) {
|
if (wasLoading.getAndSet(false)) {
|
||||||
selectAndLoadVideo(serviceId, url, name);
|
selectAndLoadVideo(serviceId, url, name);
|
||||||
|
} else if (currentInfo != null) {
|
||||||
|
updateProgressInfo(currentInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,8 +276,10 @@ public class VideoDetailFragment
|
|||||||
PreferenceManager.getDefaultSharedPreferences(activity)
|
PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
.unregisterOnSharedPreferenceChangeListener(this);
|
.unregisterOnSharedPreferenceChangeListener(this);
|
||||||
|
|
||||||
|
if (positionSubscriber != null) positionSubscriber.dispose();
|
||||||
if (currentWorker != null) currentWorker.dispose();
|
if (currentWorker != null) currentWorker.dispose();
|
||||||
if (disposables != null) disposables.clear();
|
if (disposables != null) disposables.clear();
|
||||||
|
positionSubscriber = null;
|
||||||
currentWorker = null;
|
currentWorker = null;
|
||||||
disposables = null;
|
disposables = null;
|
||||||
}
|
}
|
||||||
@ -462,6 +472,7 @@ public class VideoDetailFragment
|
|||||||
videoTitleTextView = rootView.findViewById(R.id.detail_video_title_view);
|
videoTitleTextView = rootView.findViewById(R.id.detail_video_title_view);
|
||||||
videoTitleToggleArrow = rootView.findViewById(R.id.detail_toggle_description_view);
|
videoTitleToggleArrow = rootView.findViewById(R.id.detail_toggle_description_view);
|
||||||
videoCountView = rootView.findViewById(R.id.detail_view_count_view);
|
videoCountView = rootView.findViewById(R.id.detail_view_count_view);
|
||||||
|
positionView = rootView.findViewById(R.id.position_view);
|
||||||
|
|
||||||
detailControlsBackground = rootView.findViewById(R.id.detail_controls_background);
|
detailControlsBackground = rootView.findViewById(R.id.detail_controls_background);
|
||||||
detailControlsPopup = rootView.findViewById(R.id.detail_controls_popup);
|
detailControlsPopup = rootView.findViewById(R.id.detail_controls_popup);
|
||||||
@ -469,6 +480,7 @@ public class VideoDetailFragment
|
|||||||
detailControlsDownload = rootView.findViewById(R.id.detail_controls_download);
|
detailControlsDownload = rootView.findViewById(R.id.detail_controls_download);
|
||||||
appendControlsDetail = rootView.findViewById(R.id.touch_append_detail);
|
appendControlsDetail = rootView.findViewById(R.id.touch_append_detail);
|
||||||
detailDurationView = rootView.findViewById(R.id.detail_duration_view);
|
detailDurationView = rootView.findViewById(R.id.detail_duration_view);
|
||||||
|
detailPositionView = rootView.findViewById(R.id.detail_position_view);
|
||||||
|
|
||||||
videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout);
|
videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout);
|
||||||
videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view);
|
videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view);
|
||||||
@ -536,10 +548,10 @@ public class VideoDetailFragment
|
|||||||
final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> {
|
final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> {
|
||||||
switch (i) {
|
switch (i) {
|
||||||
case 0:
|
case 0:
|
||||||
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
|
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item), true);
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
NavigationHelper.enqueueOnPopupPlayer(getActivity(), new SinglePlayQueue(item));
|
NavigationHelper.enqueueOnPopupPlayer(getActivity(), new SinglePlayQueue(item), true);
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
if (getFragmentManager() != null) {
|
if (getFragmentManager() != null) {
|
||||||
@ -890,11 +902,11 @@ public class VideoDetailFragment
|
|||||||
|
|
||||||
final PlayQueue itemQueue = new SinglePlayQueue(currentInfo);
|
final PlayQueue itemQueue = new SinglePlayQueue(currentInfo);
|
||||||
if (append) {
|
if (append) {
|
||||||
NavigationHelper.enqueueOnPopupPlayer(activity, itemQueue);
|
NavigationHelper.enqueueOnPopupPlayer(activity, itemQueue, false);
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(activity, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
|
Toast.makeText(activity, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
|
||||||
final Intent intent = NavigationHelper.getPlayerIntent(
|
final Intent intent = NavigationHelper.getPlayerIntent(
|
||||||
activity, PopupVideoPlayer.class, itemQueue, getSelectedVideoStream().resolution
|
activity, PopupVideoPlayer.class, itemQueue, getSelectedVideoStream().resolution, true
|
||||||
);
|
);
|
||||||
activity.startService(intent);
|
activity.startService(intent);
|
||||||
}
|
}
|
||||||
@ -914,9 +926,9 @@ public class VideoDetailFragment
|
|||||||
private void openNormalBackgroundPlayer(final boolean append) {
|
private void openNormalBackgroundPlayer(final boolean append) {
|
||||||
final PlayQueue itemQueue = new SinglePlayQueue(currentInfo);
|
final PlayQueue itemQueue = new SinglePlayQueue(currentInfo);
|
||||||
if (append) {
|
if (append) {
|
||||||
NavigationHelper.enqueueOnBackgroundPlayer(activity, itemQueue);
|
NavigationHelper.enqueueOnBackgroundPlayer(activity, itemQueue, false);
|
||||||
} else {
|
} else {
|
||||||
NavigationHelper.playOnBackgroundPlayer(activity, itemQueue);
|
NavigationHelper.playOnBackgroundPlayer(activity, itemQueue, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -926,7 +938,7 @@ public class VideoDetailFragment
|
|||||||
mIntent = NavigationHelper.getPlayerIntent(activity,
|
mIntent = NavigationHelper.getPlayerIntent(activity,
|
||||||
MainVideoPlayer.class,
|
MainVideoPlayer.class,
|
||||||
playQueue,
|
playQueue,
|
||||||
getSelectedVideoStream().getResolution());
|
getSelectedVideoStream().getResolution(), true);
|
||||||
startActivity(mIntent);
|
startActivity(mIntent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1032,6 +1044,8 @@ public class VideoDetailFragment
|
|||||||
animateView(spinnerToolbar, false, 200);
|
animateView(spinnerToolbar, false, 200);
|
||||||
animateView(thumbnailPlayButton, false, 50);
|
animateView(thumbnailPlayButton, false, 50);
|
||||||
animateView(detailDurationView, false, 100);
|
animateView(detailDurationView, false, 100);
|
||||||
|
animateView(detailPositionView, false, 100);
|
||||||
|
animateView(positionView, false, 50);
|
||||||
|
|
||||||
videoTitleTextView.setText(name != null ? name : "");
|
videoTitleTextView.setText(name != null ? name : "");
|
||||||
videoTitleTextView.setMaxLines(1);
|
videoTitleTextView.setMaxLines(1);
|
||||||
@ -1146,6 +1160,7 @@ public class VideoDetailFragment
|
|||||||
videoUploadDateView.setText(Localization.localizeDate(activity, info.getUploadDate()));
|
videoUploadDateView.setText(Localization.localizeDate(activity, info.getUploadDate()));
|
||||||
}
|
}
|
||||||
prepareDescription(info.getDescription());
|
prepareDescription(info.getDescription());
|
||||||
|
updateProgressInfo(info);
|
||||||
|
|
||||||
animateView(spinnerToolbar, true, 500);
|
animateView(spinnerToolbar, true, 500);
|
||||||
setupActionBar(info);
|
setupActionBar(info);
|
||||||
@ -1195,7 +1210,7 @@ public class VideoDetailFragment
|
|||||||
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
|
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
|
||||||
downloadDialog.setSubtitleStreams(currentInfo.getSubtitles());
|
downloadDialog.setSubtitleStreams(currentInfo.getSubtitles());
|
||||||
|
|
||||||
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
downloadDialog.show(getActivity().getSupportFragmentManager(), "downloadDialog");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
ErrorActivity.ErrorInfo info = ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
|
ErrorActivity.ErrorInfo info = ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
|
||||||
ServiceList.all()
|
ServiceList.all()
|
||||||
@ -1238,5 +1253,36 @@ public class VideoDetailFragment
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateProgressInfo(@NonNull final StreamInfo info) {
|
||||||
|
if (positionSubscriber != null) {
|
||||||
|
positionSubscriber.dispose();
|
||||||
|
}
|
||||||
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||||
|
final boolean playbackResumeEnabled =
|
||||||
|
prefs.getBoolean(activity.getString(R.string.enable_watch_history_key), true)
|
||||||
|
&& prefs.getBoolean(activity.getString(R.string.enable_playback_resume_key), true);
|
||||||
|
if (!playbackResumeEnabled || info.getDuration() <= 0) {
|
||||||
|
positionView.setVisibility(View.INVISIBLE);
|
||||||
|
detailPositionView.setVisibility(View.GONE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext());
|
||||||
|
positionSubscriber = recordManager.loadStreamState(info)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.onErrorComplete()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(state -> {
|
||||||
|
final int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime());
|
||||||
|
positionView.setMax((int) info.getDuration());
|
||||||
|
positionView.setProgressAnimated(seconds);
|
||||||
|
detailPositionView.setText(Localization.getDurationString(seconds));
|
||||||
|
animateView(positionView, true, 500);
|
||||||
|
animateView(detailPositionView, true, 500);
|
||||||
|
}, e -> {
|
||||||
|
if (DEBUG) e.printStackTrace();
|
||||||
|
}, () -> {
|
||||||
|
animateView(positionView, false, 500);
|
||||||
|
animateView(detailPositionView, false, 500);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,6 +65,12 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||||||
infoListAdapter = new InfoListAdapter(activity);
|
infoListAdapter = new InfoListAdapter(activity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDetach() {
|
||||||
|
infoListAdapter.dispose();
|
||||||
|
super.onDetach();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@ -94,6 +100,8 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||||||
}
|
}
|
||||||
updateFlags = 0;
|
updateFlags = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
itemsList.post(infoListAdapter::updateStates);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
@ -266,13 +274,13 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
|||||||
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
|
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
|
||||||
switch (i) {
|
switch (i) {
|
||||||
case 0:
|
case 0:
|
||||||
NavigationHelper.playOnBackgroundPlayer(context, new SinglePlayQueue(item));
|
NavigationHelper.playOnBackgroundPlayer(context, new SinglePlayQueue(item), true);
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
|
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item), true);
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
|
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item), true);
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
if (getFragmentManager() != null) {
|
if (getFragmentManager() != null) {
|
||||||
|
@ -170,19 +170,19 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||||||
final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0);
|
final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0);
|
||||||
switch (i) {
|
switch (i) {
|
||||||
case 0:
|
case 0:
|
||||||
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
|
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item), false);
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
|
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item), false);
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
|
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index), true);
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
|
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index), true);
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
|
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index), true);
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 5:
|
||||||
if (getFragmentManager() != null) {
|
if (getFragmentManager() != null) {
|
||||||
@ -440,11 +440,11 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
|||||||
monitorSubscription(result);
|
monitorSubscription(result);
|
||||||
|
|
||||||
headerPlayAllButton.setOnClickListener(
|
headerPlayAllButton.setOnClickListener(
|
||||||
view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false));
|
||||||
headerPopupButton.setOnClickListener(
|
headerPopupButton.setOnClickListener(
|
||||||
view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
|
view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
|
||||||
headerBackgroundButton.setOnClickListener(
|
headerBackgroundButton.setOnClickListener(
|
||||||
view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
|
view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlayQueue getPlayQueue() {
|
private PlayQueue getPlayQueue() {
|
||||||
|
@ -154,22 +154,22 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
|||||||
final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0);
|
final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0);
|
||||||
switch (i) {
|
switch (i) {
|
||||||
case 0:
|
case 0:
|
||||||
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
|
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item), false);
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
|
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item), false);
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
|
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index), true);
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
|
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index), true);
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
|
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index), true);
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 5:
|
||||||
ShareUtils.shareUrl(this.getContext(), item.getName(), item.getUrl());
|
ShareUtils.shareUrl(requireContext(), item.getName(), item.getUrl());
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@ -301,19 +301,19 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
|||||||
.subscribe(getPlaylistBookmarkSubscriber());
|
.subscribe(getPlaylistBookmarkSubscriber());
|
||||||
|
|
||||||
headerPlayAllButton.setOnClickListener(view ->
|
headerPlayAllButton.setOnClickListener(view ->
|
||||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false));
|
||||||
headerPopupButton.setOnClickListener(view ->
|
headerPopupButton.setOnClickListener(view ->
|
||||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
|
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
|
||||||
headerBackgroundButton.setOnClickListener(view ->
|
headerBackgroundButton.setOnClickListener(view ->
|
||||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
|
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||||
|
|
||||||
headerPopupButton.setOnLongClickListener(view -> {
|
headerPopupButton.setOnLongClickListener(view -> {
|
||||||
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue());
|
NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
headerBackgroundButton.setOnLongClickListener(view -> {
|
headerBackgroundButton.setOnLongClickListener(view -> {
|
||||||
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue());
|
NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,14 @@ package org.schabi.newpipe.info_list;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
@ -59,13 +61,14 @@ public class InfoItemBuilder {
|
|||||||
this.context = context;
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem) {
|
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, @Nullable StreamStateEntity state) {
|
||||||
return buildView(parent, infoItem, false);
|
return buildView(parent, infoItem, state, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, boolean useMiniVariant) {
|
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem,
|
||||||
|
@Nullable StreamStateEntity state, boolean useMiniVariant) {
|
||||||
InfoItemHolder holder = holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
|
InfoItemHolder holder = holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
|
||||||
holder.updateFromItem(infoItem);
|
holder.updateFromItem(infoItem, state);
|
||||||
return holder.itemView;
|
return holder.itemView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,22 +1,25 @@
|
|||||||
package org.schabi.newpipe.info_list;
|
package org.schabi.newpipe.info_list;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v7.widget.GridLayoutManager;
|
import android.support.v7.widget.GridLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
||||||
@ -50,7 +53,7 @@ import java.util.List;
|
|||||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
public class InfoListAdapter extends StateObjectsListAdapter {
|
||||||
private static final String TAG = InfoListAdapter.class.getSimpleName();
|
private static final String TAG = InfoListAdapter.class.getSimpleName();
|
||||||
private static final boolean DEBUG = false;
|
private static final boolean DEBUG = false;
|
||||||
|
|
||||||
@ -87,6 +90,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
|||||||
}
|
}
|
||||||
|
|
||||||
public InfoListAdapter(Activity a) {
|
public InfoListAdapter(Activity a) {
|
||||||
|
super(a.getApplicationContext());
|
||||||
infoItemBuilder = new InfoItemBuilder(a);
|
infoItemBuilder = new InfoItemBuilder(a);
|
||||||
infoItemList = new ArrayList<>();
|
infoItemList = new ArrayList<>();
|
||||||
}
|
}
|
||||||
@ -115,50 +119,64 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
|||||||
this.useGridVariant = useGridVariant;
|
this.useGridVariant = useGridVariant;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addInfoItemList(List<InfoItem> data) {
|
public void addInfoItemList(@Nullable final List<InfoItem> data) {
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
if (DEBUG) {
|
loadStates(data, infoItemList.size(), () -> addInfoItemListImpl(data));
|
||||||
Log.d(TAG, "addInfoItemList() before > infoItemList.size() = " + infoItemList.size() + ", data.size() = " + data.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
int offsetStart = sizeConsideringHeaderOffset();
|
|
||||||
infoItemList.addAll(data);
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter);
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyItemRangeInserted(offsetStart, data.size());
|
|
||||||
|
|
||||||
if (footer != null && showFooter) {
|
|
||||||
int footerNow = sizeConsideringHeaderOffset();
|
|
||||||
notifyItemMoved(offsetStart, footerNow);
|
|
||||||
|
|
||||||
if (DEBUG) Log.d(TAG, "addInfoItemList() footer from " + offsetStart + " to " + footerNow);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addInfoItem(InfoItem data) {
|
private void addInfoItemListImpl(@NonNull List<InfoItem> data) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "addInfoItemList() before > infoItemList.size() = " + infoItemList.size() + ", data.size() = " + data.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
int offsetStart = sizeConsideringHeaderOffset();
|
||||||
|
infoItemList.addAll(data);
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter);
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyItemRangeInserted(offsetStart, data.size());
|
||||||
|
|
||||||
|
if (footer != null && showFooter) {
|
||||||
|
int footerNow = sizeConsideringHeaderOffset();
|
||||||
|
notifyItemMoved(offsetStart, footerNow);
|
||||||
|
|
||||||
|
if (DEBUG) Log.d(TAG, "addInfoItemList() footer from " + offsetStart + " to " + footerNow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addInfoItem(@Nullable InfoItem data) {
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
if (DEBUG) {
|
loadState(data, infoItemList.size(), () -> addInfoItemImpl(data));
|
||||||
Log.d(TAG, "addInfoItem() before > infoItemList.size() = " + infoItemList.size() + ", thread = " + Thread.currentThread());
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int positionInserted = sizeConsideringHeaderOffset();
|
private void addInfoItemImpl(@NonNull InfoItem data) {
|
||||||
infoItemList.add(data);
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "addInfoItem() before > infoItemList.size() = " + infoItemList.size() + ", thread = " + Thread.currentThread());
|
||||||
|
}
|
||||||
|
|
||||||
if (DEBUG) {
|
int positionInserted = sizeConsideringHeaderOffset();
|
||||||
Log.d(TAG, "addInfoItem() after > position = " + positionInserted + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter);
|
infoItemList.add(data);
|
||||||
}
|
|
||||||
notifyItemInserted(positionInserted);
|
|
||||||
|
|
||||||
if (footer != null && showFooter) {
|
if (DEBUG) {
|
||||||
int footerNow = sizeConsideringHeaderOffset();
|
Log.d(TAG, "addInfoItem() after > position = " + positionInserted + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter);
|
||||||
notifyItemMoved(positionInserted, footerNow);
|
}
|
||||||
|
notifyItemInserted(positionInserted);
|
||||||
|
|
||||||
if (DEBUG) Log.d(TAG, "addInfoItem() footer from " + positionInserted + " to " + footerNow);
|
if (footer != null && showFooter) {
|
||||||
}
|
int footerNow = sizeConsideringHeaderOffset();
|
||||||
|
notifyItemMoved(positionInserted, footerNow);
|
||||||
|
|
||||||
|
if (DEBUG) Log.d(TAG, "addInfoItem() footer from " + positionInserted + " to " + footerNow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateStates() {
|
||||||
|
if (!infoItemList.isEmpty()) {
|
||||||
|
updateAllStates(infoItemList);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,6 +185,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
infoItemList.clear();
|
infoItemList.clear();
|
||||||
|
clearStates();
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,8 +259,9 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) {
|
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int type) {
|
||||||
if (DEBUG)
|
if (DEBUG)
|
||||||
Log.d(TAG, "onCreateViewHolder() called with: parent = [" + parent + "], type = [" + type + "]");
|
Log.d(TAG, "onCreateViewHolder() called with: parent = [" + parent + "], type = [" + type + "]");
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -278,13 +298,13 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
|
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||||
if (DEBUG) Log.d(TAG, "onBindViewHolder() called with: holder = [" + holder.getClass().getSimpleName() + "], position = [" + position + "]");
|
if (DEBUG) Log.d(TAG, "onBindViewHolder() called with: holder = [" + holder.getClass().getSimpleName() + "], position = [" + position + "]");
|
||||||
if (holder instanceof InfoItemHolder) {
|
if (holder instanceof InfoItemHolder) {
|
||||||
// If header isn't null, offset the items by -1
|
// If header isn't null, offset the items by -1
|
||||||
if (header != null) position--;
|
if (header != null) position--;
|
||||||
|
|
||||||
((InfoItemHolder) holder).updateFromItem(infoItemList.get(position));
|
((InfoItemHolder) holder).updateFromItem(infoItemList.get(position), getState(position));
|
||||||
} else if (holder instanceof HFHolder && position == 0 && header != null) {
|
} else if (holder instanceof HFHolder && position == 0 && header != null) {
|
||||||
((HFHolder) holder).view = header;
|
((HFHolder) holder).view = header;
|
||||||
} else if (holder instanceof HFHolder && position == sizeConsideringHeaderOffset() && footer != null && showFooter) {
|
} else if (holder instanceof HFHolder && position == sizeConsideringHeaderOffset() && footer != null && showFooter) {
|
||||||
@ -292,6 +312,28 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) {
|
||||||
|
if (!payloads.isEmpty() && holder instanceof InfoItemHolder) {
|
||||||
|
for (Object payload : payloads) {
|
||||||
|
if (payload instanceof StreamStateEntity) {
|
||||||
|
((InfoItemHolder) holder).updateState(infoItemList.get(header == null ? position : position - 1),
|
||||||
|
(StreamStateEntity) payload);
|
||||||
|
} else if (payload instanceof Boolean) {
|
||||||
|
((InfoItemHolder) holder).updateState(infoItemList.get(header == null ? position : position - 1),
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onBindViewHolder(holder, position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onItemStateChanged(int position, @Nullable StreamStateEntity state) {
|
||||||
|
notifyItemChanged(header == null ? position : position + 1, state != null ? state : false);
|
||||||
|
}
|
||||||
|
|
||||||
public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final int spanCount) {
|
public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final int spanCount) {
|
||||||
return new GridLayoutManager.SpanSizeLookup() {
|
return new GridLayoutManager.SpanSizeLookup() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -0,0 +1,189 @@
|
|||||||
|
package org.schabi.newpipe.info_list;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.util.SparseArray;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.BuildConfig;
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
|
import org.schabi.newpipe.util.SparseArrayUtils;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
|
|
||||||
|
public abstract class StateObjectsListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||||
|
|
||||||
|
private final SparseArray<StreamStateEntity> states;
|
||||||
|
private final HistoryRecordManager recordManager;
|
||||||
|
private final CompositeDisposable stateLoaders;
|
||||||
|
private final Context context;
|
||||||
|
|
||||||
|
public StateObjectsListAdapter(Context context) {
|
||||||
|
this.states = new SparseArray<>();
|
||||||
|
this.recordManager = new HistoryRecordManager(context);
|
||||||
|
this.context = context;
|
||||||
|
this.stateLoaders = new CompositeDisposable();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public StreamStateEntity getState(int position) {
|
||||||
|
return states.get(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void clearStates() {
|
||||||
|
states.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendStates(List<StreamStateEntity> statesEntities, int offset) {
|
||||||
|
for (int i = 0; i < statesEntities.size(); i++) {
|
||||||
|
final StreamStateEntity state = statesEntities.get(i);
|
||||||
|
if (state != null) {
|
||||||
|
states.append(offset + i, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendState(StreamStateEntity statesEntity, int offset) {
|
||||||
|
if (statesEntity != null) {
|
||||||
|
states.append(offset, statesEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void removeState(int index) {
|
||||||
|
states.remove(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void moveState(int from, int to) {
|
||||||
|
final StreamStateEntity item = states.get(from);
|
||||||
|
if (from < to) {
|
||||||
|
SparseArrayUtils.shiftItemsDown(states, from, to);
|
||||||
|
} else {
|
||||||
|
SparseArrayUtils.shiftItemsUp(states, to, from);
|
||||||
|
}
|
||||||
|
states.put(to, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void loadStates(List<InfoItem> list, int offset, Runnable callback) {
|
||||||
|
if (isPlaybackStatesVisible()) {
|
||||||
|
stateLoaders.add(
|
||||||
|
recordManager.loadStreamStateBatch(list)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(streamStateEntities -> {
|
||||||
|
appendStates(streamStateEntities, offset);
|
||||||
|
callback.run();
|
||||||
|
}, throwable -> {
|
||||||
|
if (BuildConfig.DEBUG) throwable.printStackTrace();
|
||||||
|
callback.run();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
callback.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void loadState(InfoItem item, int offset, Runnable callback) {
|
||||||
|
if (isPlaybackStatesVisible()) {
|
||||||
|
stateLoaders.add(
|
||||||
|
recordManager.loadStreamState(item)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(streamStateEntities -> {
|
||||||
|
appendState(streamStateEntities[0], offset);
|
||||||
|
callback.run();
|
||||||
|
}, throwable -> {
|
||||||
|
if (BuildConfig.DEBUG) throwable.printStackTrace();
|
||||||
|
callback.run();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
callback.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void loadStatesForLocal(List<? extends LocalItem> list, int offset, Runnable callback) {
|
||||||
|
if (isPlaybackStatesVisible()) {
|
||||||
|
stateLoaders.add(
|
||||||
|
recordManager.loadLocalStreamStateBatch(list)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(streamStateEntities -> {
|
||||||
|
appendStates(streamStateEntities, offset);
|
||||||
|
callback.run();
|
||||||
|
}, throwable -> {
|
||||||
|
if (BuildConfig.DEBUG) throwable.printStackTrace();
|
||||||
|
callback.run();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
callback.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processStatesUpdates(List<StreamStateEntity> streamStateEntities) {
|
||||||
|
for (int i = 0; i < streamStateEntities.size(); i++) {
|
||||||
|
final StreamStateEntity newState = streamStateEntities.get(i);
|
||||||
|
if (!Objects.equals(states.get(i), newState)) {
|
||||||
|
if (newState == null) {
|
||||||
|
states.remove(i);
|
||||||
|
} else {
|
||||||
|
states.put(i, newState);
|
||||||
|
}
|
||||||
|
onItemStateChanged(i, newState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void updateAllStates(List<InfoItem> list) {
|
||||||
|
if (isPlaybackStatesVisible()) {
|
||||||
|
stateLoaders.add(
|
||||||
|
recordManager.loadStreamStateBatch(list)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(this::processStatesUpdates, throwable -> {
|
||||||
|
if (BuildConfig.DEBUG) throwable.printStackTrace();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final int[] positions = SparseArrayUtils.getKeys(states);
|
||||||
|
states.clear();
|
||||||
|
for (int pos : positions) onItemStateChanged(pos, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void updateAllLocalStates(List<? extends LocalItem> list) {
|
||||||
|
if (isPlaybackStatesVisible()) {
|
||||||
|
stateLoaders.add(
|
||||||
|
recordManager.loadLocalStreamStateBatch(list)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(this::processStatesUpdates, throwable -> {
|
||||||
|
if (BuildConfig.DEBUG) throwable.printStackTrace();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final int[] positions = SparseArrayUtils.getKeys(states);
|
||||||
|
states.clear();
|
||||||
|
for (int pos : positions) onItemStateChanged(pos, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dispose() {
|
||||||
|
stateLoaders.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isPlaybackStatesVisible() {
|
||||||
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)
|
||||||
|
&& prefs.getBoolean(context.getString(R.string.enable_playback_resume_key), true)
|
||||||
|
&& prefs.getBoolean(context.getString(R.string.enable_playback_state_lists_key), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void onItemStateChanged(int position, @Nullable StreamStateEntity state);
|
||||||
|
|
||||||
|
}
|
@ -1,9 +1,11 @@
|
|||||||
package org.schabi.newpipe.info_list.holder;
|
package org.schabi.newpipe.info_list.holder;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
@ -38,8 +40,8 @@ public class ChannelInfoItemHolder extends ChannelMiniInfoItemHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateFromItem(final InfoItem infoItem) {
|
public void updateFromItem(final InfoItem infoItem, @Nullable final StreamStateEntity state) {
|
||||||
super.updateFromItem(infoItem);
|
super.updateFromItem(infoItem, state);
|
||||||
|
|
||||||
if (!(infoItem instanceof ChannelInfoItem)) return;
|
if (!(infoItem instanceof ChannelInfoItem)) return;
|
||||||
final ChannelInfoItem item = (ChannelInfoItem) infoItem;
|
final ChannelInfoItem item = (ChannelInfoItem) infoItem;
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
package org.schabi.newpipe.info_list.holder;
|
package org.schabi.newpipe.info_list.holder;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
@ -30,7 +32,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateFromItem(final InfoItem infoItem) {
|
public void updateFromItem(final InfoItem infoItem, @Nullable final StreamStateEntity state) {
|
||||||
if (!(infoItem instanceof ChannelInfoItem)) return;
|
if (!(infoItem instanceof ChannelInfoItem)) return;
|
||||||
final ChannelInfoItem item = (ChannelInfoItem) infoItem;
|
final ChannelInfoItem item = (ChannelInfoItem) infoItem;
|
||||||
|
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
package org.schabi.newpipe.info_list.holder;
|
package org.schabi.newpipe.info_list.holder;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
import org.schabi.newpipe.util.Localization;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 12.02.17.
|
* Created by Christian Schabesberger on 12.02.17.
|
||||||
@ -41,8 +41,8 @@ public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateFromItem(final InfoItem infoItem) {
|
public void updateFromItem(final InfoItem infoItem, @Nullable final StreamStateEntity state) {
|
||||||
super.updateFromItem(infoItem);
|
super.updateFromItem(infoItem, state);
|
||||||
|
|
||||||
if (!(infoItem instanceof CommentsInfoItem)) return;
|
if (!(infoItem instanceof CommentsInfoItem)) return;
|
||||||
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.schabi.newpipe.info_list.holder;
|
package org.schabi.newpipe.info_list.holder;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
import android.support.v7.app.AppCompatActivity;
|
||||||
import android.text.util.Linkify;
|
import android.text.util.Linkify;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@ -8,6 +9,7 @@ import android.widget.TextView;
|
|||||||
|
|
||||||
import org.jsoup.helper.StringUtil;
|
import org.jsoup.helper.StringUtil;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
@ -65,7 +67,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateFromItem(final InfoItem infoItem) {
|
public void updateFromItem(final InfoItem infoItem, @Nullable final StreamStateEntity state) {
|
||||||
if (!(infoItem instanceof CommentsInfoItem)) return;
|
if (!(infoItem instanceof CommentsInfoItem)) return;
|
||||||
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
||||||
|
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
package org.schabi.newpipe.info_list.holder;
|
package org.schabi.newpipe.info_list.holder;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
|
|
||||||
@ -35,5 +37,8 @@ public abstract class InfoItemHolder extends RecyclerView.ViewHolder {
|
|||||||
this.itemBuilder = infoItemBuilder;
|
this.itemBuilder = infoItemBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract void updateFromItem(final InfoItem infoItem);
|
public abstract void updateFromItem(final InfoItem infoItem, @Nullable final StreamStateEntity state);
|
||||||
|
|
||||||
|
public void updateState(final InfoItem infoItem, @Nullable final StreamStateEntity state) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
package org.schabi.newpipe.info_list.holder;
|
package org.schabi.newpipe.info_list.holder;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
@ -30,7 +32,7 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateFromItem(final InfoItem infoItem) {
|
public void updateFromItem(final InfoItem infoItem, @Nullable final StreamStateEntity state) {
|
||||||
if (!(infoItem instanceof PlaylistInfoItem)) return;
|
if (!(infoItem instanceof PlaylistInfoItem)) return;
|
||||||
final PlaylistInfoItem item = (PlaylistInfoItem) infoItem;
|
final PlaylistInfoItem item = (PlaylistInfoItem) infoItem;
|
||||||
|
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
package org.schabi.newpipe.info_list.holder;
|
package org.schabi.newpipe.info_list.holder;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
@ -40,8 +42,8 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateFromItem(final InfoItem infoItem) {
|
public void updateFromItem(final InfoItem infoItem, @Nullable final StreamStateEntity state) {
|
||||||
super.updateFromItem(infoItem);
|
super.updateFromItem(infoItem, state);
|
||||||
|
|
||||||
if (!(infoItem instanceof StreamInfoItem)) return;
|
if (!(infoItem instanceof StreamInfoItem)) return;
|
||||||
final StreamInfoItem item = (StreamInfoItem) infoItem;
|
final StreamInfoItem item = (StreamInfoItem) infoItem;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.schabi.newpipe.info_list.holder;
|
package org.schabi.newpipe.info_list.holder;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.content.ContextCompat;
|
import android.support.v4.content.ContextCompat;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@ -7,12 +8,17 @@ import android.widget.ImageView;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
|
import org.schabi.newpipe.util.AnimationUtils;
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||||
|
|
||||||
@ -20,6 +26,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
public final TextView itemVideoTitleView;
|
public final TextView itemVideoTitleView;
|
||||||
public final TextView itemUploaderView;
|
public final TextView itemUploaderView;
|
||||||
public final TextView itemDurationView;
|
public final TextView itemDurationView;
|
||||||
|
public final AnimatedProgressBar itemProgressView;
|
||||||
|
|
||||||
StreamMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
|
StreamMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
|
||||||
super(infoItemBuilder, layoutId, parent);
|
super(infoItemBuilder, layoutId, parent);
|
||||||
@ -28,6 +35,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView);
|
itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView);
|
||||||
itemUploaderView = itemView.findViewById(R.id.itemUploaderView);
|
itemUploaderView = itemView.findViewById(R.id.itemUploaderView);
|
||||||
itemDurationView = itemView.findViewById(R.id.itemDurationView);
|
itemDurationView = itemView.findViewById(R.id.itemDurationView);
|
||||||
|
itemProgressView = itemView.findViewById(R.id.itemProgressView);
|
||||||
}
|
}
|
||||||
|
|
||||||
public StreamMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
|
public StreamMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||||
@ -35,7 +43,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateFromItem(final InfoItem infoItem) {
|
public void updateFromItem(final InfoItem infoItem, @Nullable final StreamStateEntity state) {
|
||||||
if (!(infoItem instanceof StreamInfoItem)) return;
|
if (!(infoItem instanceof StreamInfoItem)) return;
|
||||||
final StreamInfoItem item = (StreamInfoItem) infoItem;
|
final StreamInfoItem item = (StreamInfoItem) infoItem;
|
||||||
|
|
||||||
@ -47,13 +55,22 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
||||||
R.color.duration_background_color));
|
R.color.duration_background_color));
|
||||||
itemDurationView.setVisibility(View.VISIBLE);
|
itemDurationView.setVisibility(View.VISIBLE);
|
||||||
|
if (state != null) {
|
||||||
|
itemProgressView.setVisibility(View.VISIBLE);
|
||||||
|
itemProgressView.setMax((int) item.getDuration());
|
||||||
|
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
|
||||||
|
} else {
|
||||||
|
itemProgressView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
} else if (item.getStreamType() == StreamType.LIVE_STREAM) {
|
} else if (item.getStreamType() == StreamType.LIVE_STREAM) {
|
||||||
itemDurationView.setText(R.string.duration_live);
|
itemDurationView.setText(R.string.duration_live);
|
||||||
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
||||||
R.color.live_duration_background_color));
|
R.color.live_duration_background_color));
|
||||||
itemDurationView.setVisibility(View.VISIBLE);
|
itemDurationView.setVisibility(View.VISIBLE);
|
||||||
|
itemProgressView.setVisibility(View.GONE);
|
||||||
} else {
|
} else {
|
||||||
itemDurationView.setVisibility(View.GONE);
|
itemDurationView.setVisibility(View.GONE);
|
||||||
|
itemProgressView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||||
@ -83,6 +100,22 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateState(final InfoItem infoItem, @Nullable final StreamStateEntity state) {
|
||||||
|
final StreamInfoItem item = (StreamInfoItem) infoItem;
|
||||||
|
if (state != null && item.getDuration() > 0 && item.getStreamType() != StreamType.LIVE_STREAM) {
|
||||||
|
itemProgressView.setMax((int) item.getDuration());
|
||||||
|
if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||||
|
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
|
||||||
|
} else {
|
||||||
|
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
|
||||||
|
AnimationUtils.animateView(itemProgressView, true, 500);
|
||||||
|
}
|
||||||
|
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||||
|
AnimationUtils.animateView(itemProgressView, false, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void enableLongClick(final StreamInfoItem item) {
|
private void enableLongClick(final StreamInfoItem item) {
|
||||||
itemView.setLongClickable(true);
|
itemView.setLongClickable(true);
|
||||||
itemView.setOnLongClickListener(view -> {
|
itemView.setOnLongClickListener(view -> {
|
||||||
|
@ -76,6 +76,8 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
|||||||
}
|
}
|
||||||
updateFlags = 0;
|
updateFlags = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
itemsList.post(itemListAdapter::updateStates);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
@ -150,6 +152,7 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
|||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
itemsList = null;
|
itemsList = null;
|
||||||
|
itemListAdapter.dispose();
|
||||||
itemListAdapter = null;
|
itemListAdapter = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package org.schabi.newpipe.local;
|
package org.schabi.newpipe.local;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v7.widget.GridLayoutManager;
|
import android.support.v7.widget.GridLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@ -8,6 +10,8 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
|
import org.schabi.newpipe.info_list.StateObjectsListAdapter;
|
||||||
import org.schabi.newpipe.local.holder.LocalItemHolder;
|
import org.schabi.newpipe.local.holder.LocalItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.LocalPlaylistGridItemHolder;
|
import org.schabi.newpipe.local.holder.LocalPlaylistGridItemHolder;
|
||||||
import org.schabi.newpipe.local.holder.LocalPlaylistItemHolder;
|
import org.schabi.newpipe.local.holder.LocalPlaylistItemHolder;
|
||||||
@ -45,7 +49,7 @@ import java.util.List;
|
|||||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
public class LocalItemListAdapter extends StateObjectsListAdapter {
|
||||||
|
|
||||||
private static final String TAG = LocalItemListAdapter.class.getSimpleName();
|
private static final String TAG = LocalItemListAdapter.class.getSimpleName();
|
||||||
private static final boolean DEBUG = false;
|
private static final boolean DEBUG = false;
|
||||||
@ -72,6 +76,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
private View footer = null;
|
private View footer = null;
|
||||||
|
|
||||||
public LocalItemListAdapter(Activity activity) {
|
public LocalItemListAdapter(Activity activity) {
|
||||||
|
super(activity.getApplicationContext());
|
||||||
localItemBuilder = new LocalItemBuilder(activity);
|
localItemBuilder = new LocalItemBuilder(activity);
|
||||||
localItems = new ArrayList<>();
|
localItems = new ArrayList<>();
|
||||||
dateFormat = DateFormat.getDateInstance(DateFormat.SHORT,
|
dateFormat = DateFormat.getDateInstance(DateFormat.SHORT,
|
||||||
@ -86,39 +91,49 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
localItemBuilder.setOnItemSelectedListener(null);
|
localItemBuilder.setOnItemSelectedListener(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addItems(List<? extends LocalItem> data) {
|
public void addItems(@Nullable List<? extends LocalItem> data) {
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
if (DEBUG) {
|
loadStatesForLocal(data, localItems.size(), () -> addItemsImpl(data));
|
||||||
Log.d(TAG, "addItems() before > localItems.size() = " +
|
}
|
||||||
localItems.size() + ", data.size() = " + data.size());
|
}
|
||||||
}
|
|
||||||
|
|
||||||
int offsetStart = sizeConsideringHeader();
|
private void addItemsImpl(@NonNull List<? extends LocalItem> data) {
|
||||||
localItems.addAll(data);
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "addItems() before > localItems.size() = " +
|
||||||
|
localItems.size() + ", data.size() = " + data.size());
|
||||||
|
}
|
||||||
|
|
||||||
if (DEBUG) {
|
int offsetStart = sizeConsideringHeader();
|
||||||
Log.d(TAG, "addItems() after > offsetStart = " + offsetStart +
|
localItems.addAll(data);
|
||||||
", localItems.size() = " + localItems.size() +
|
|
||||||
", header = " + header + ", footer = " + footer +
|
|
||||||
", showFooter = " + showFooter);
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyItemRangeInserted(offsetStart, data.size());
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "addItems() after > offsetStart = " + offsetStart +
|
||||||
|
", localItems.size() = " + localItems.size() +
|
||||||
|
", header = " + header + ", footer = " + footer +
|
||||||
|
", showFooter = " + showFooter);
|
||||||
|
}
|
||||||
|
|
||||||
if (footer != null && showFooter) {
|
notifyItemRangeInserted(offsetStart, data.size());
|
||||||
int footerNow = sizeConsideringHeader();
|
|
||||||
notifyItemMoved(offsetStart, footerNow);
|
|
||||||
|
|
||||||
if (DEBUG) Log.d(TAG, "addItems() footer from " + offsetStart +
|
if (footer != null && showFooter) {
|
||||||
" to " + footerNow);
|
int footerNow = sizeConsideringHeader();
|
||||||
}
|
notifyItemMoved(offsetStart, footerNow);
|
||||||
|
|
||||||
|
if (DEBUG) Log.d(TAG, "addItems() footer from " + offsetStart +
|
||||||
|
" to " + footerNow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateStates() {
|
||||||
|
if (!localItems.isEmpty()) {
|
||||||
|
updateAllLocalStates(localItems);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeItem(final LocalItem data) {
|
public void removeItem(final LocalItem data) {
|
||||||
final int index = localItems.indexOf(data);
|
final int index = localItems.indexOf(data);
|
||||||
|
|
||||||
localItems.remove(index);
|
localItems.remove(index);
|
||||||
|
removeState(index);
|
||||||
notifyItemRemoved(index + (header != null ? 1 : 0));
|
notifyItemRemoved(index + (header != null ? 1 : 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,6 +145,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
if (actualFrom >= localItems.size() || actualTo >= localItems.size()) return false;
|
if (actualFrom >= localItems.size() || actualTo >= localItems.size()) return false;
|
||||||
|
|
||||||
localItems.add(actualTo, localItems.remove(actualFrom));
|
localItems.add(actualTo, localItems.remove(actualFrom));
|
||||||
|
moveState(actualFrom, actualTo);
|
||||||
notifyItemMoved(fromAdapterPosition, toAdapterPosition);
|
notifyItemMoved(fromAdapterPosition, toAdapterPosition);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -139,6 +155,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
localItems.clear();
|
localItems.clear();
|
||||||
|
clearStates();
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,7 +276,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
// If header isn't null, offset the items by -1
|
// If header isn't null, offset the items by -1
|
||||||
if (header != null) position--;
|
if (header != null) position--;
|
||||||
|
|
||||||
((LocalItemHolder) holder).updateFromItem(localItems.get(position), dateFormat);
|
((LocalItemHolder) holder).updateFromItem(localItems.get(position), getState(position), dateFormat);
|
||||||
} else if (holder instanceof HeaderFooterHolder && position == 0 && header != null) {
|
} else if (holder instanceof HeaderFooterHolder && position == 0 && header != null) {
|
||||||
((HeaderFooterHolder) holder).view = header;
|
((HeaderFooterHolder) holder).view = header;
|
||||||
} else if (holder instanceof HeaderFooterHolder && position == sizeConsideringHeader()
|
} else if (holder instanceof HeaderFooterHolder && position == sizeConsideringHeader()
|
||||||
@ -268,6 +285,28 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) {
|
||||||
|
if (!payloads.isEmpty() && holder instanceof LocalItemHolder) {
|
||||||
|
for (Object payload : payloads) {
|
||||||
|
if (payload instanceof StreamStateEntity) {
|
||||||
|
((LocalItemHolder) holder).updateState(localItems.get(header == null ? position : position - 1),
|
||||||
|
(StreamStateEntity) payload);
|
||||||
|
} else if (payload instanceof Boolean) {
|
||||||
|
((LocalItemHolder) holder).updateState(localItems.get(header == null ? position : position - 1),
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onBindViewHolder(holder, position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onItemStateChanged(int position, @Nullable StreamStateEntity state) {
|
||||||
|
notifyItemChanged(header == null ? position : position + 1, state != null ? state : false);
|
||||||
|
}
|
||||||
|
|
||||||
public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final int spanCount) {
|
public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final int spanCount) {
|
||||||
return new GridLayoutManager.SpanSizeLookup() {
|
return new GridLayoutManager.SpanSizeLookup() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -112,7 +112,10 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
|||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
if (playlistReactor != null) playlistReactor.dispose();
|
if (playlistReactor != null) playlistReactor.dispose();
|
||||||
if (playlistAdapter != null) playlistAdapter.unsetSelectedListener();
|
if (playlistAdapter != null) {
|
||||||
|
playlistAdapter.dispose();
|
||||||
|
playlistAdapter.unsetSelectedListener();
|
||||||
|
}
|
||||||
|
|
||||||
playlistReactor = null;
|
playlistReactor = null;
|
||||||
playlistRecyclerView = null;
|
playlistRecyclerView = null;
|
||||||
|
@ -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.
|
||||||
|
@ -26,23 +26,29 @@ import android.support.annotation.NonNull;
|
|||||||
import org.schabi.newpipe.NewPipeDatabase;
|
import org.schabi.newpipe.NewPipeDatabase;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.AppDatabase;
|
import org.schabi.newpipe.database.AppDatabase;
|
||||||
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
|
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
|
||||||
import org.schabi.newpipe.database.history.dao.StreamHistoryDAO;
|
import org.schabi.newpipe.database.history.dao.StreamHistoryDAO;
|
||||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
|
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
|
||||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
|
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
|
||||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||||
import org.schabi.newpipe.database.stream.dao.StreamDAO;
|
import org.schabi.newpipe.database.stream.dao.StreamDAO;
|
||||||
import org.schabi.newpipe.database.stream.dao.StreamStateDAO;
|
import org.schabi.newpipe.database.stream.dao.StreamStateDAO;
|
||||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reactivex.Completable;
|
||||||
import io.reactivex.Flowable;
|
import io.reactivex.Flowable;
|
||||||
import io.reactivex.Maybe;
|
import io.reactivex.Maybe;
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
@ -80,9 +86,9 @@ public class HistoryRecordManager {
|
|||||||
final Date currentTime = new Date();
|
final Date currentTime = new Date();
|
||||||
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
|
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
|
||||||
final long streamId = streamTable.upsert(new StreamEntity(info));
|
final long streamId = streamTable.upsert(new StreamEntity(info));
|
||||||
StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry();
|
StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId);
|
||||||
|
|
||||||
if (latestEntry != null && latestEntry.getStreamUid() == streamId) {
|
if (latestEntry != null) {
|
||||||
streamHistoryTable.delete(latestEntry);
|
streamHistoryTable.delete(latestEntry);
|
||||||
latestEntry.setAccessDate(currentTime);
|
latestEntry.setAccessDate(currentTime);
|
||||||
latestEntry.setRepeatCount(latestEntry.getRepeatCount() + 1);
|
latestEntry.setRepeatCount(latestEntry.getRepeatCount() + 1);
|
||||||
@ -99,7 +105,7 @@ public class HistoryRecordManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Single<Integer> deleteWholeStreamHistory() {
|
public Single<Integer> deleteWholeStreamHistory() {
|
||||||
return Single.fromCallable(() -> streamHistoryTable.deleteAll())
|
return Single.fromCallable(streamHistoryTable::deleteAll)
|
||||||
.subscribeOn(Schedulers.io());
|
.subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,7 +166,7 @@ public class HistoryRecordManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Single<Integer> deleteWholeSearchHistory() {
|
public Single<Integer> deleteWholeSearchHistory() {
|
||||||
return Single.fromCallable(() -> searchHistoryTable.deleteAll())
|
return Single.fromCallable(searchHistoryTable::deleteAll)
|
||||||
.subscribeOn(Schedulers.io());
|
.subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,21 +186,104 @@ public class HistoryRecordManager {
|
|||||||
// Stream State History
|
// Stream State History
|
||||||
///////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
public Maybe<StreamHistoryEntity> getStreamHistory(final StreamInfo info) {
|
||||||
public Maybe<StreamStateEntity> loadStreamState(final StreamInfo info) {
|
return Maybe.fromCallable(() -> {
|
||||||
return Maybe.fromCallable(() -> streamTable.upsert(new StreamEntity(info)))
|
final long streamId = streamTable.upsert(new StreamEntity(info));
|
||||||
.flatMap(streamId -> streamStateTable.getState(streamId).firstElement())
|
return streamHistoryTable.getLatestEntry(streamId);
|
||||||
.flatMap(states -> states.isEmpty() ? Maybe.empty() : Maybe.just(states.get(0)))
|
}).subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Maybe<StreamStateEntity> loadStreamState(final PlayQueueItem queueItem) {
|
||||||
|
return queueItem.getStream()
|
||||||
|
.map((info) -> streamTable.upsert(new StreamEntity(info)))
|
||||||
|
.flatMapPublisher(streamStateTable::getState)
|
||||||
|
.firstElement()
|
||||||
|
.flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0)))
|
||||||
|
.filter(state -> state.isValid((int) queueItem.getDuration()))
|
||||||
.subscribeOn(Schedulers.io());
|
.subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Maybe<Long> saveStreamState(@NonNull final StreamInfo info, final long progressTime) {
|
public Maybe<StreamStateEntity> loadStreamState(final StreamInfo info) {
|
||||||
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
|
return Single.fromCallable(() -> streamTable.upsert(new StreamEntity(info)))
|
||||||
|
.flatMapPublisher(streamStateTable::getState)
|
||||||
|
.firstElement()
|
||||||
|
.flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0)))
|
||||||
|
.filter(state -> state.isValid((int) info.getDuration()))
|
||||||
|
.subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Completable saveStreamState(@NonNull final StreamInfo info, final long progressTime) {
|
||||||
|
return Completable.fromAction(() -> database.runInTransaction(() -> {
|
||||||
final long streamId = streamTable.upsert(new StreamEntity(info));
|
final long streamId = streamTable.upsert(new StreamEntity(info));
|
||||||
return streamStateTable.upsert(new StreamStateEntity(streamId, progressTime));
|
final StreamStateEntity state = new StreamStateEntity(streamId, progressTime);
|
||||||
|
if (state.isValid((int) info.getDuration())) {
|
||||||
|
streamStateTable.upsert(state);
|
||||||
|
} else {
|
||||||
|
streamStateTable.deleteState(streamId);
|
||||||
|
}
|
||||||
})).subscribeOn(Schedulers.io());
|
})).subscribeOn(Schedulers.io());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Single<StreamStateEntity[]> loadStreamState(final InfoItem info) {
|
||||||
|
return Single.fromCallable(() -> {
|
||||||
|
final List<StreamEntity> entities = streamTable.getStream(info.getServiceId(), info.getUrl()).blockingFirst();
|
||||||
|
if (entities.isEmpty()) {
|
||||||
|
return new StreamStateEntity[]{null};
|
||||||
|
}
|
||||||
|
final List<StreamStateEntity> states = streamStateTable.getState(entities.get(0).getUid()).blockingFirst();
|
||||||
|
if (states.isEmpty()) {
|
||||||
|
return new StreamStateEntity[]{null};
|
||||||
|
}
|
||||||
|
return new StreamStateEntity[]{states.get(0)};
|
||||||
|
}).subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Single<List<StreamStateEntity>> loadStreamStateBatch(final List<InfoItem> infos) {
|
||||||
|
return Single.fromCallable(() -> {
|
||||||
|
final List<StreamStateEntity> result = new ArrayList<>(infos.size());
|
||||||
|
for (InfoItem info : infos) {
|
||||||
|
final List<StreamEntity> entities = streamTable.getStream(info.getServiceId(), info.getUrl()).blockingFirst();
|
||||||
|
if (entities.isEmpty()) {
|
||||||
|
result.add(null);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final List<StreamStateEntity> states = streamStateTable.getState(entities.get(0).getUid()).blockingFirst();
|
||||||
|
if (states.isEmpty()) {
|
||||||
|
result.add(null);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result.add(states.get(0));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}).subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Single<List<StreamStateEntity>> loadLocalStreamStateBatch(final List<? extends LocalItem> items) {
|
||||||
|
return Single.fromCallable(() -> {
|
||||||
|
final List<StreamStateEntity> result = new ArrayList<>(items.size());
|
||||||
|
for (LocalItem item : items) {
|
||||||
|
long streamId;
|
||||||
|
if (item instanceof StreamStatisticsEntry) {
|
||||||
|
streamId = ((StreamStatisticsEntry) item).streamId;
|
||||||
|
} else if (item instanceof PlaylistStreamEntity) {
|
||||||
|
streamId = ((PlaylistStreamEntity) item).getStreamUid();
|
||||||
|
} else if (item instanceof PlaylistStreamEntry) {
|
||||||
|
streamId = ((PlaylistStreamEntry) item).streamId;
|
||||||
|
} else {
|
||||||
|
result.add(null);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final List<StreamStateEntity> states = streamStateTable.getState(streamId).blockingFirst();
|
||||||
|
if (states.isEmpty()) {
|
||||||
|
result.add(null);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result.add(states.get(0));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}).subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////
|
||||||
// Utility
|
// Utility
|
||||||
///////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////
|
||||||
|
@ -310,11 +310,11 @@ public class StatisticsPlaylistFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
headerPlayAllButton.setOnClickListener(view ->
|
headerPlayAllButton.setOnClickListener(view ->
|
||||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false));
|
||||||
headerPopupButton.setOnClickListener(view ->
|
headerPopupButton.setOnClickListener(view ->
|
||||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
|
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
|
||||||
headerBackgroundButton.setOnClickListener(view ->
|
headerBackgroundButton.setOnClickListener(view ->
|
||||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
|
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||||
sortButton.setOnClickListener(view -> toggleSortMode());
|
sortButton.setOnClickListener(view -> toggleSortMode());
|
||||||
|
|
||||||
hideLoading();
|
hideLoading();
|
||||||
@ -377,19 +377,19 @@ public class StatisticsPlaylistFragment
|
|||||||
final int index = Math.max(itemListAdapter.getItemsList().indexOf(item), 0);
|
final int index = Math.max(itemListAdapter.getItemsList().indexOf(item), 0);
|
||||||
switch (i) {
|
switch (i) {
|
||||||
case 0:
|
case 0:
|
||||||
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(infoItem));
|
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(infoItem), false);
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(infoItem));
|
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(infoItem), false);
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
|
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index), true);
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
|
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index), true);
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
|
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index), true);
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 5:
|
||||||
deleteEntry(index);
|
deleteEntry(index);
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
package org.schabi.newpipe.local.holder;
|
package org.schabi.newpipe.local.holder;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
|
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
@ -38,5 +40,8 @@ public abstract class LocalItemHolder extends RecyclerView.ViewHolder {
|
|||||||
this.itemBuilder = itemBuilder;
|
this.itemBuilder = itemBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract void updateFromItem(final LocalItem item, final DateFormat dateFormat);
|
public abstract void updateFromItem(final LocalItem item, @Nullable final StreamStateEntity state, final DateFormat dateFormat);
|
||||||
|
|
||||||
|
public void updateState(final LocalItem localItem, @Nullable final StreamStateEntity state) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
package org.schabi.newpipe.local.holder;
|
package org.schabi.newpipe.local.holder;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||||
|
|
||||||
@ -21,7 +23,7 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
|
public void updateFromItem(final LocalItem localItem, @Nullable final StreamStateEntity state, final DateFormat dateFormat) {
|
||||||
if (!(localItem instanceof PlaylistMetadataEntry)) return;
|
if (!(localItem instanceof PlaylistMetadataEntry)) return;
|
||||||
final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem;
|
final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem;
|
||||||
|
|
||||||
@ -32,6 +34,6 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
|
|||||||
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView,
|
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView,
|
||||||
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
|
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
|
||||||
|
|
||||||
super.updateFromItem(localItem, dateFormat);
|
super.updateFromItem(localItem, state, dateFormat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.schabi.newpipe.local.holder;
|
package org.schabi.newpipe.local.holder;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.content.ContextCompat;
|
import android.support.v4.content.ContextCompat;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@ -10,12 +11,16 @@ import android.widget.TextView;
|
|||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
|
import org.schabi.newpipe.util.AnimationUtils;
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||||
|
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
||||||
|
|
||||||
@ -24,6 +29,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
|||||||
public final TextView itemAdditionalDetailsView;
|
public final TextView itemAdditionalDetailsView;
|
||||||
public final TextView itemDurationView;
|
public final TextView itemDurationView;
|
||||||
public final View itemHandleView;
|
public final View itemHandleView;
|
||||||
|
public final AnimatedProgressBar itemProgressView;
|
||||||
|
|
||||||
LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
|
LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
|
||||||
super(infoItemBuilder, layoutId, parent);
|
super(infoItemBuilder, layoutId, parent);
|
||||||
@ -33,6 +39,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
|||||||
itemAdditionalDetailsView = itemView.findViewById(R.id.itemAdditionalDetails);
|
itemAdditionalDetailsView = itemView.findViewById(R.id.itemAdditionalDetails);
|
||||||
itemDurationView = itemView.findViewById(R.id.itemDurationView);
|
itemDurationView = itemView.findViewById(R.id.itemDurationView);
|
||||||
itemHandleView = itemView.findViewById(R.id.itemHandle);
|
itemHandleView = itemView.findViewById(R.id.itemHandle);
|
||||||
|
itemProgressView = itemView.findViewById(R.id.itemProgressView);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) {
|
public LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||||
@ -40,7 +47,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
|
public void updateFromItem(final LocalItem localItem, @Nullable final StreamStateEntity state, final DateFormat dateFormat) {
|
||||||
if (!(localItem instanceof PlaylistStreamEntry)) return;
|
if (!(localItem instanceof PlaylistStreamEntry)) return;
|
||||||
final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem;
|
final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem;
|
||||||
|
|
||||||
@ -53,6 +60,13 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
|||||||
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
||||||
R.color.duration_background_color));
|
R.color.duration_background_color));
|
||||||
itemDurationView.setVisibility(View.VISIBLE);
|
itemDurationView.setVisibility(View.VISIBLE);
|
||||||
|
if (state != null) {
|
||||||
|
itemProgressView.setVisibility(View.VISIBLE);
|
||||||
|
itemProgressView.setMax((int) item.duration);
|
||||||
|
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
|
||||||
|
} else {
|
||||||
|
itemProgressView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
itemDurationView.setVisibility(View.GONE);
|
itemDurationView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
@ -79,6 +93,23 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
|||||||
itemHandleView.setOnTouchListener(getOnTouchListener(item));
|
itemHandleView.setOnTouchListener(getOnTouchListener(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateState(LocalItem localItem, @Nullable StreamStateEntity state) {
|
||||||
|
if (!(localItem instanceof PlaylistStreamEntry)) return;
|
||||||
|
final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem;
|
||||||
|
if (state != null && item.duration > 0) {
|
||||||
|
itemProgressView.setMax((int) item.duration);
|
||||||
|
if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||||
|
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
|
||||||
|
} else {
|
||||||
|
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
|
||||||
|
AnimationUtils.animateView(itemProgressView, true, 500);
|
||||||
|
}
|
||||||
|
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||||
|
AnimationUtils.animateView(itemProgressView, false, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private View.OnTouchListener getOnTouchListener(final PlaylistStreamEntry item) {
|
private View.OnTouchListener getOnTouchListener(final PlaylistStreamEntry item) {
|
||||||
return (view, motionEvent) -> {
|
return (view, motionEvent) -> {
|
||||||
view.performClick();
|
view.performClick();
|
||||||
|
@ -10,12 +10,16 @@ import android.widget.TextView;
|
|||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
|
import org.schabi.newpipe.util.AnimationUtils;
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||||
|
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 01.08.16.
|
* Created by Christian Schabesberger on 01.08.16.
|
||||||
@ -45,6 +49,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
|||||||
public final TextView itemDurationView;
|
public final TextView itemDurationView;
|
||||||
@Nullable
|
@Nullable
|
||||||
public final TextView itemAdditionalDetails;
|
public final TextView itemAdditionalDetails;
|
||||||
|
public final AnimatedProgressBar itemProgressView;
|
||||||
|
|
||||||
public LocalStatisticStreamItemHolder(LocalItemBuilder itemBuilder, ViewGroup parent) {
|
public LocalStatisticStreamItemHolder(LocalItemBuilder itemBuilder, ViewGroup parent) {
|
||||||
this(itemBuilder, R.layout.list_stream_item, parent);
|
this(itemBuilder, R.layout.list_stream_item, parent);
|
||||||
@ -58,6 +63,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
|||||||
itemUploaderView = itemView.findViewById(R.id.itemUploaderView);
|
itemUploaderView = itemView.findViewById(R.id.itemUploaderView);
|
||||||
itemDurationView = itemView.findViewById(R.id.itemDurationView);
|
itemDurationView = itemView.findViewById(R.id.itemDurationView);
|
||||||
itemAdditionalDetails = itemView.findViewById(R.id.itemAdditionalDetails);
|
itemAdditionalDetails = itemView.findViewById(R.id.itemAdditionalDetails);
|
||||||
|
itemProgressView = itemView.findViewById(R.id.itemProgressView);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getStreamInfoDetailLine(final StreamStatisticsEntry entry,
|
private String getStreamInfoDetailLine(final StreamStatisticsEntry entry,
|
||||||
@ -70,7 +76,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
|
public void updateFromItem(final LocalItem localItem, @Nullable final StreamStateEntity state, final DateFormat dateFormat) {
|
||||||
if (!(localItem instanceof StreamStatisticsEntry)) return;
|
if (!(localItem instanceof StreamStatisticsEntry)) return;
|
||||||
final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem;
|
final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem;
|
||||||
|
|
||||||
@ -82,8 +88,16 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
|||||||
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
||||||
R.color.duration_background_color));
|
R.color.duration_background_color));
|
||||||
itemDurationView.setVisibility(View.VISIBLE);
|
itemDurationView.setVisibility(View.VISIBLE);
|
||||||
|
if (state != null) {
|
||||||
|
itemProgressView.setVisibility(View.VISIBLE);
|
||||||
|
itemProgressView.setMax((int) item.duration);
|
||||||
|
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
|
||||||
|
} else {
|
||||||
|
itemProgressView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
itemDurationView.setVisibility(View.GONE);
|
itemDurationView.setVisibility(View.GONE);
|
||||||
|
itemProgressView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemAdditionalDetails != null) {
|
if (itemAdditionalDetails != null) {
|
||||||
@ -108,4 +122,21 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateState(LocalItem localItem, @Nullable StreamStateEntity state) {
|
||||||
|
if (!(localItem instanceof StreamStatisticsEntry)) return;
|
||||||
|
final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem;
|
||||||
|
if (state != null && item.duration > 0) {
|
||||||
|
itemProgressView.setMax((int) item.duration);
|
||||||
|
if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||||
|
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
|
||||||
|
} else {
|
||||||
|
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
|
||||||
|
AnimationUtils.animateView(itemProgressView, true, 500);
|
||||||
|
}
|
||||||
|
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||||
|
AnimationUtils.animateView(itemProgressView, false, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
package org.schabi.newpipe.local.holder;
|
package org.schabi.newpipe.local.holder;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
|
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
@ -31,7 +33,7 @@ public abstract class PlaylistItemHolder extends LocalItemHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
|
public void updateFromItem(final LocalItem localItem, @Nullable final StreamStateEntity state, final DateFormat dateFormat) {
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
if (itemBuilder.getOnItemSelectedListener() != null) {
|
if (itemBuilder.getOnItemSelectedListener() != null) {
|
||||||
itemBuilder.getOnItemSelectedListener().selected(localItem);
|
itemBuilder.getOnItemSelectedListener().selected(localItem);
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
package org.schabi.newpipe.local.holder;
|
package org.schabi.newpipe.local.holder;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||||
@ -21,7 +23,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
|
public void updateFromItem(final LocalItem localItem, @Nullable final StreamStateEntity state, final DateFormat dateFormat) {
|
||||||
if (!(localItem instanceof PlaylistRemoteEntity)) return;
|
if (!(localItem instanceof PlaylistRemoteEntity)) return;
|
||||||
final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem;
|
final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem;
|
||||||
|
|
||||||
@ -33,6 +35,6 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
|||||||
itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView,
|
itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView,
|
||||||
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
|
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
|
||||||
|
|
||||||
super.updateFromItem(localItem, dateFormat);
|
super.updateFromItem(localItem, state, dateFormat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -319,11 +319,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
setVideoCount(itemListAdapter.getItemsList().size());
|
setVideoCount(itemListAdapter.getItemsList().size());
|
||||||
|
|
||||||
headerPlayAllButton.setOnClickListener(view ->
|
headerPlayAllButton.setOnClickListener(view ->
|
||||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false));
|
||||||
headerPopupButton.setOnClickListener(view ->
|
headerPopupButton.setOnClickListener(view ->
|
||||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
|
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
|
||||||
headerBackgroundButton.setOnClickListener(view ->
|
headerBackgroundButton.setOnClickListener(view ->
|
||||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
|
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||||
|
|
||||||
hideLoading();
|
hideLoading();
|
||||||
}
|
}
|
||||||
@ -534,20 +534,20 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
switch (i) {
|
switch (i) {
|
||||||
case 0:
|
case 0:
|
||||||
NavigationHelper.enqueueOnBackgroundPlayer(context,
|
NavigationHelper.enqueueOnBackgroundPlayer(context,
|
||||||
new SinglePlayQueue(infoItem));
|
new SinglePlayQueue(infoItem), false);
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
NavigationHelper.enqueueOnPopupPlayer(activity, new
|
NavigationHelper.enqueueOnPopupPlayer(activity, new
|
||||||
SinglePlayQueue(infoItem));
|
SinglePlayQueue(infoItem), false);
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
|
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index), true);
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
|
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index), true);
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
|
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index), true);
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 5:
|
||||||
changeThumbnailUrl(item.thumbnailUrl);
|
changeThumbnailUrl(item.thumbnailUrl);
|
||||||
|
@ -23,7 +23,6 @@ import android.support.annotation.Nullable;
|
|||||||
import android.support.v4.app.FragmentManager;
|
import android.support.v4.app.FragmentManager;
|
||||||
import android.support.v4.content.LocalBroadcastManager;
|
import android.support.v4.content.LocalBroadcastManager;
|
||||||
import android.support.v7.app.ActionBar;
|
import android.support.v7.app.ActionBar;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
|
||||||
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;
|
||||||
@ -48,10 +47,8 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
|||||||
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
||||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||||
import org.schabi.newpipe.report.UserAction;
|
|
||||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService;
|
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService;
|
||||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService;
|
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService;
|
||||||
import org.schabi.newpipe.report.ErrorActivity;
|
|
||||||
import org.schabi.newpipe.report.UserAction;
|
import org.schabi.newpipe.report.UserAction;
|
||||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
@ -131,6 +128,12 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
|||||||
subscriptionService = SubscriptionService.getInstance(activity);
|
subscriptionService = SubscriptionService.getInstance(activity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDetach() {
|
||||||
|
infoListAdapter.dispose();
|
||||||
|
super.onDetach();
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
|
||||||
@ -150,6 +153,8 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
|||||||
}
|
}
|
||||||
updateFlags = 0;
|
updateFlags = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
itemsList.post(infoListAdapter::updateStates);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -150,6 +150,7 @@ public final class BackgroundPlayer extends Service {
|
|||||||
lockManager.releaseWifiAndCpu();
|
lockManager.releaseWifiAndCpu();
|
||||||
}
|
}
|
||||||
if (basePlayerImpl != null) {
|
if (basePlayerImpl != null) {
|
||||||
|
basePlayerImpl.savePlaybackState();
|
||||||
basePlayerImpl.stopActivityBinding();
|
basePlayerImpl.stopActivityBinding();
|
||||||
basePlayerImpl.destroy();
|
basePlayerImpl.destroy();
|
||||||
}
|
}
|
||||||
|
@ -23,9 +23,11 @@ import android.content.BroadcastReceiver;
|
|||||||
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.SharedPreferences;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@ -145,6 +147,8 @@ public abstract class BasePlayer implements
|
|||||||
@NonNull
|
@NonNull
|
||||||
public static final String APPEND_ONLY = "append_only";
|
public static final String APPEND_ONLY = "append_only";
|
||||||
@NonNull
|
@NonNull
|
||||||
|
public static final String RESUME_PLAYBACK = "resume_playback";
|
||||||
|
@NonNull
|
||||||
public static final String SELECT_ON_APPEND = "select_on_append";
|
public static final String SELECT_ON_APPEND = "select_on_append";
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
@ -279,8 +283,23 @@ public abstract class BasePlayer implements
|
|||||||
) {
|
) {
|
||||||
simpleExoPlayer.seekTo(playQueue.getIndex(), queue.getItem().getRecoveryPosition());
|
simpleExoPlayer.seekTo(playQueue.getIndex(), queue.getItem().getRecoveryPosition());
|
||||||
return;
|
return;
|
||||||
|
} else if (intent.getBooleanExtra(RESUME_PLAYBACK, false) && isPlaybackResumeEnabled()) {
|
||||||
|
final PlayQueueItem item = queue.getItem();
|
||||||
|
if (item != null && item.getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET && isPlaybackResumeEnabled()) {
|
||||||
|
final Disposable stateLoader = recordManager.loadStreamState(item)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.doFinally(() -> initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence,
|
||||||
|
/*playOnInit=*/true))
|
||||||
|
.subscribe(
|
||||||
|
state -> queue.setRecovery(queue.getIndex(), state.getProgressTime()),
|
||||||
|
error -> {
|
||||||
|
if (DEBUG) error.printStackTrace();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
databaseUpdateReactor.add(stateLoader);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Good to go...
|
// Good to go...
|
||||||
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence,
|
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence,
|
||||||
/*playOnInit=*/true);
|
/*playOnInit=*/true);
|
||||||
@ -615,6 +634,9 @@ public abstract class BasePlayer implements
|
|||||||
break;
|
break;
|
||||||
case Player.STATE_ENDED: // 4
|
case Player.STATE_ENDED: // 4
|
||||||
changeState(STATE_COMPLETED);
|
changeState(STATE_COMPLETED);
|
||||||
|
if (currentMetadata != null) {
|
||||||
|
resetPlaybackState(currentMetadata.getMetadata());
|
||||||
|
}
|
||||||
isPrepared = false;
|
isPrepared = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -721,6 +743,7 @@ public abstract class BasePlayer implements
|
|||||||
case DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
|
case DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
|
||||||
case DISCONTINUITY_REASON_INTERNAL:
|
case DISCONTINUITY_REASON_INTERNAL:
|
||||||
if (playQueue.getIndex() != newWindowIndex) {
|
if (playQueue.getIndex() != newWindowIndex) {
|
||||||
|
resetPlaybackState(playQueue.getItem());
|
||||||
playQueue.setIndex(newWindowIndex);
|
playQueue.setIndex(newWindowIndex);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -750,6 +773,9 @@ public abstract class BasePlayer implements
|
|||||||
@Override
|
@Override
|
||||||
public void onSeekProcessed() {
|
public void onSeekProcessed() {
|
||||||
if (DEBUG) Log.d(TAG, "ExoPlayer - onSeekProcessed() called");
|
if (DEBUG) Log.d(TAG, "ExoPlayer - onSeekProcessed() called");
|
||||||
|
if (isPrepared) {
|
||||||
|
savePlaybackState();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Playback Listener
|
// Playback Listener
|
||||||
@ -1017,27 +1043,40 @@ public abstract class BasePlayer implements
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void savePlaybackState(final StreamInfo info, final long progress) {
|
private void savePlaybackState(final StreamInfo info, final long progress) {
|
||||||
if (info == null) return;
|
if (info == null) return;
|
||||||
|
if (DEBUG) Log.d(TAG, "savePlaybackState() called");
|
||||||
final Disposable stateSaver = recordManager.saveStreamState(info, progress)
|
final Disposable stateSaver = recordManager.saveStreamState(info, progress)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.doOnError((e) -> {
|
||||||
|
if (DEBUG) e.printStackTrace();
|
||||||
|
})
|
||||||
.onErrorComplete()
|
.onErrorComplete()
|
||||||
.subscribe(
|
.subscribe();
|
||||||
ignored -> {/* successful */},
|
|
||||||
error -> Log.e(TAG, "savePlaybackState() failure: ", error)
|
|
||||||
);
|
|
||||||
databaseUpdateReactor.add(stateSaver);
|
databaseUpdateReactor.add(stateSaver);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void savePlaybackState() {
|
private void resetPlaybackState(final PlayQueueItem queueItem) {
|
||||||
|
if (queueItem == null) return;
|
||||||
|
final Disposable stateSaver = queueItem.getStream()
|
||||||
|
.flatMapCompletable(info -> recordManager.saveStreamState(info, 0))
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.doOnError((e) -> {
|
||||||
|
if (DEBUG) e.printStackTrace();
|
||||||
|
})
|
||||||
|
.onErrorComplete()
|
||||||
|
.subscribe();
|
||||||
|
databaseUpdateReactor.add(stateSaver);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetPlaybackState(final StreamInfo info) {
|
||||||
|
savePlaybackState(info, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void savePlaybackState() {
|
||||||
if (simpleExoPlayer == null || currentMetadata == null) return;
|
if (simpleExoPlayer == null || currentMetadata == null) return;
|
||||||
final StreamInfo currentInfo = currentMetadata.getMetadata();
|
final StreamInfo currentInfo = currentMetadata.getMetadata();
|
||||||
|
savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition());
|
||||||
if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD_MILLIS &&
|
|
||||||
simpleExoPlayer.getCurrentPosition() <
|
|
||||||
simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD_MILLIS) {
|
|
||||||
savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void maybeUpdateCurrentMetadata() {
|
private void maybeUpdateCurrentMetadata() {
|
||||||
@ -1225,4 +1264,10 @@ public abstract class BasePlayer implements
|
|||||||
public boolean gotDestroyed() {
|
public boolean gotDestroyed() {
|
||||||
return simpleExoPlayer == null;
|
return simpleExoPlayer == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isPlaybackResumeEnabled() {
|
||||||
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)
|
||||||
|
&& prefs.getBoolean(context.getString(R.string.enable_playback_resume_key), true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -248,6 +248,12 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||||||
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(newBase));
|
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(newBase));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
playerImpl.savePlaybackState();
|
||||||
|
super.onPause();
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// State Saving
|
// State Saving
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
@ -583,7 +589,8 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||||||
this.getPlaybackSpeed(),
|
this.getPlaybackSpeed(),
|
||||||
this.getPlaybackPitch(),
|
this.getPlaybackPitch(),
|
||||||
this.getPlaybackSkipSilence(),
|
this.getPlaybackSkipSilence(),
|
||||||
this.getPlaybackQuality()
|
this.getPlaybackQuality(),
|
||||||
|
false
|
||||||
);
|
);
|
||||||
context.startService(intent);
|
context.startService(intent);
|
||||||
|
|
||||||
@ -605,7 +612,8 @@ public final class MainVideoPlayer extends AppCompatActivity
|
|||||||
this.getPlaybackSpeed(),
|
this.getPlaybackSpeed(),
|
||||||
this.getPlaybackPitch(),
|
this.getPlaybackPitch(),
|
||||||
this.getPlaybackSkipSilence(),
|
this.getPlaybackSkipSilence(),
|
||||||
this.getPlaybackQuality()
|
this.getPlaybackQuality(),
|
||||||
|
false
|
||||||
);
|
);
|
||||||
context.startService(intent);
|
context.startService(intent);
|
||||||
|
|
||||||
|
@ -325,6 +325,7 @@ public final class PopupVideoPlayer extends Service {
|
|||||||
isPopupClosing = true;
|
isPopupClosing = true;
|
||||||
|
|
||||||
if (playerImpl != null) {
|
if (playerImpl != null) {
|
||||||
|
playerImpl.savePlaybackState();
|
||||||
if (playerImpl.getRootView() != null) {
|
if (playerImpl.getRootView() != null) {
|
||||||
windowManager.removeView(playerImpl.getRootView());
|
windowManager.removeView(playerImpl.getRootView());
|
||||||
}
|
}
|
||||||
@ -565,7 +566,8 @@ public final class PopupVideoPlayer extends Service {
|
|||||||
this.getPlaybackSpeed(),
|
this.getPlaybackSpeed(),
|
||||||
this.getPlaybackPitch(),
|
this.getPlaybackPitch(),
|
||||||
this.getPlaybackSkipSilence(),
|
this.getPlaybackSkipSilence(),
|
||||||
this.getPlaybackQuality()
|
this.getPlaybackQuality(),
|
||||||
|
false
|
||||||
);
|
);
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
context.startActivity(intent);
|
context.startActivity(intent);
|
||||||
|
@ -188,7 +188,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
|||||||
this.player.getPlaybackSpeed(),
|
this.player.getPlaybackSpeed(),
|
||||||
this.player.getPlaybackPitch(),
|
this.player.getPlaybackPitch(),
|
||||||
this.player.getPlaybackSkipSilence(),
|
this.player.getPlaybackSkipSilence(),
|
||||||
null
|
null,
|
||||||
|
false
|
||||||
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -297,6 +297,7 @@ public abstract class VideoPlayer extends BasePlayer
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add all available captions
|
||||||
for (int i = 0; i < availableLanguages.size(); i++) {
|
for (int i = 0; i < availableLanguages.size(); i++) {
|
||||||
final String captionLanguage = availableLanguages.get(i);
|
final String captionLanguage = availableLanguages.get(i);
|
||||||
MenuItem captionItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId,
|
MenuItem captionItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId,
|
||||||
@ -506,7 +507,7 @@ public abstract class VideoPlayer extends BasePlayer
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Normalize mismatching language strings
|
// Normalize mismatching language strings
|
||||||
final String preferredLanguage = trackSelector.getParameters().preferredTextLanguage;
|
final String preferredLanguage = trackSelector.getPreferredTextLanguage();
|
||||||
// Build UI
|
// Build UI
|
||||||
buildCaptionMenu(availableLanguages);
|
buildCaptionMenu(availableLanguages);
|
||||||
if (trackSelector.getParameters().getRendererDisabled(textRenderer) ||
|
if (trackSelector.getParameters().getRendererDisabled(textRenderer) ||
|
||||||
@ -541,6 +542,11 @@ public abstract class VideoPlayer extends BasePlayer
|
|||||||
playbackSpeedTextView.setText(formatSpeed(getPlaybackSpeed()));
|
playbackSpeedTextView.setText(formatSpeed(getPlaybackSpeed()));
|
||||||
|
|
||||||
super.onPrepared(playWhenReady);
|
super.onPrepared(playWhenReady);
|
||||||
|
|
||||||
|
if (simpleExoPlayer.getCurrentPosition() != 0 && !isControlsVisible()) {
|
||||||
|
controlsVisibilityHandler.removeCallbacksAndMessages(null);
|
||||||
|
controlsVisibilityHandler.postDelayed(this::showControlsThenHide, DEFAULT_CONTROLS_DURATION);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -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();
|
||||||
|
@ -17,7 +17,9 @@ public enum UserAction {
|
|||||||
REQUESTED_KIOSK("requested kiosk"),
|
REQUESTED_KIOSK("requested kiosk"),
|
||||||
REQUESTED_COMMENTS("requested comments"),
|
REQUESTED_COMMENTS("requested comments"),
|
||||||
DELETE_FROM_HISTORY("delete from history"),
|
DELETE_FROM_HISTORY("delete from history"),
|
||||||
PLAY_STREAM("Play stream");
|
PLAY_STREAM("Play stream"),
|
||||||
|
DOWNLOAD_POSTPROCESSING("download post-processing"),
|
||||||
|
DOWNLOAD_FAILED("download failed");
|
||||||
|
|
||||||
|
|
||||||
private final String message;
|
private final String message;
|
||||||
|
@ -1,30 +1,77 @@
|
|||||||
package org.schabi.newpipe.settings;
|
package org.schabi.newpipe.settings;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
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.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 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;
|
||||||
|
|
||||||
public class DownloadSettingsFragment extends BasePreferenceFragment {
|
import java.io.File;
|
||||||
private static final int REQUEST_DOWNLOAD_PATH = 0x1235;
|
import java.io.IOException;
|
||||||
private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236;
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
private String DOWNLOAD_PATH_PREFERENCE;
|
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||||
|
|
||||||
|
public class DownloadSettingsFragment extends BasePreferenceFragment {
|
||||||
|
private static final int REQUEST_DOWNLOAD_VIDEO_PATH = 0x1235;
|
||||||
|
private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236;
|
||||||
|
public static final boolean IGNORE_RELEASE_ON_OLD_PATH = true;
|
||||||
|
|
||||||
|
private String DOWNLOAD_PATH_VIDEO_PREFERENCE;
|
||||||
private String DOWNLOAD_PATH_AUDIO_PREFERENCE;
|
private String DOWNLOAD_PATH_AUDIO_PREFERENCE;
|
||||||
|
|
||||||
|
private String DOWNLOAD_STORAGE_ASK;
|
||||||
|
|
||||||
|
private Preference prefPathVideo;
|
||||||
|
private Preference prefPathAudio;
|
||||||
|
private Preference prefStorageAsk;
|
||||||
|
|
||||||
|
private Context ctx;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
initKeys();
|
DOWNLOAD_PATH_VIDEO_PREFERENCE = getString(R.string.download_path_video_key);
|
||||||
|
DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key);
|
||||||
|
DOWNLOAD_STORAGE_ASK = getString(R.string.downloads_storage_ask);
|
||||||
|
|
||||||
|
prefPathVideo = findPreference(DOWNLOAD_PATH_VIDEO_PREFERENCE);
|
||||||
|
prefPathAudio = findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE);
|
||||||
|
prefStorageAsk = findPreference(DOWNLOAD_STORAGE_ASK);
|
||||||
|
|
||||||
updatePreferencesSummary();
|
updatePreferencesSummary();
|
||||||
|
updatePathPickers(!defaultPreferences.getBoolean(DOWNLOAD_STORAGE_ASK, false));
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
prefStorageAsk.setSummary(R.string.downloads_storage_ask_summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasInvalidPath(DOWNLOAD_PATH_VIDEO_PREFERENCE) || hasInvalidPath(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
|
||||||
|
Toast.makeText(ctx, R.string.download_pick_path, Toast.LENGTH_SHORT).show();
|
||||||
|
updatePreferencesSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
prefStorageAsk.setOnPreferenceChangeListener((preference, value) -> {
|
||||||
|
updatePathPickers(!(boolean) value);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -32,52 +79,183 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
|
|||||||
addPreferencesFromResource(R.xml.download_settings);
|
addPreferencesFromResource(R.xml.download_settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initKeys() {
|
@Override
|
||||||
DOWNLOAD_PATH_PREFERENCE = getString(R.string.download_path_key);
|
public void onAttach(Context context) {
|
||||||
DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key);
|
super.onAttach(context);
|
||||||
|
ctx = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDetach() {
|
||||||
|
super.onDetach();
|
||||||
|
ctx = null;
|
||||||
|
prefStorageAsk.setOnPreferenceChangeListener(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updatePreferencesSummary() {
|
private void updatePreferencesSummary() {
|
||||||
findPreference(DOWNLOAD_PATH_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_PREFERENCE, getString(R.string.download_path_summary)));
|
showPathInSummary(DOWNLOAD_PATH_VIDEO_PREFERENCE, R.string.download_path_summary, prefPathVideo);
|
||||||
findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, getString(R.string.download_path_audio_summary)));
|
showPathInSummary(DOWNLOAD_PATH_AUDIO_PREFERENCE, R.string.download_path_audio_summary, prefPathAudio);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showPathInSummary(String prefKey, @StringRes int defaultString, Preference target) {
|
||||||
|
String rawUri = defaultPreferences.getString(prefKey, null);
|
||||||
|
if (rawUri == null || rawUri.isEmpty()) {
|
||||||
|
target.setSummary(getString(defaultString));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawUri.charAt(0) == File.separatorChar) {
|
||||||
|
target.setSummary(rawUri);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rawUri.startsWith(ContentResolver.SCHEME_FILE)) {
|
||||||
|
target.setSummary(new File(URI.create(rawUri)).getPath());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
rawUri = URLDecoder.decode(rawUri, StandardCharsets.UTF_8.name());
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
target.setSummary(rawUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isFileUri(String path) {
|
||||||
|
return path.charAt(0) == File.separatorChar || path.startsWith(ContentResolver.SCHEME_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasInvalidPath(String prefKey) {
|
||||||
|
String value = defaultPreferences.getString(prefKey, null);
|
||||||
|
return value == null || value.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePathPickers(boolean enabled) {
|
||||||
|
prefPathVideo.setEnabled(enabled);
|
||||||
|
prefPathAudio.setEnabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: after releasing the old path, all downloads created on the folder becomes inaccessible
|
||||||
|
private void forgetSAFTree(Context ctx, String oldPath) {
|
||||||
|
if (IGNORE_RELEASE_ON_OLD_PATH) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldPath == null || oldPath.isEmpty() || isFileUri(oldPath)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Uri uri = Uri.parse(oldPath);
|
||||||
|
|
||||||
|
ctx.getContentResolver().releasePersistableUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS);
|
||||||
|
ctx.revokeUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS);
|
||||||
|
|
||||||
|
Log.i(TAG, "Revoke old path permissions success on " + oldPath);
|
||||||
|
} catch (Exception err) {
|
||||||
|
Log.e(TAG, "Error revoking old path permissions on " + 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
|
||||||
public boolean onPreferenceTreeClick(Preference preference) {
|
public boolean onPreferenceTreeClick(Preference preference) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
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)) {
|
int request;
|
||||||
Intent i = new Intent(getActivity(), FilePickerActivityHelper.class)
|
|
||||||
|
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;
|
||||||
|
} else {
|
||||||
|
return super.onPreferenceTreeClick(preference);
|
||||||
|
}
|
||||||
|
|
||||||
|
Intent i;
|
||||||
|
if (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_PERSISTABLE_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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onPreferenceTreeClick(preference);
|
startActivityForResult(i, request);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
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;
|
||||||
updatePreferencesSummary();
|
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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
// steps:
|
||||||
|
// 1. revoke permissions on the old save path
|
||||||
|
// 2. acquire permissions on the new save path
|
||||||
|
// 3. save the new path, if step(2) was successful
|
||||||
|
final Context ctx = getContext();
|
||||||
|
if (ctx == null) throw new NullPointerException("getContext()");
|
||||||
|
|
||||||
|
forgetSAFTree(ctx, defaultPreferences.getString(key, ""));
|
||||||
|
|
||||||
|
try {
|
||||||
|
ctx.grantUriPermission(ctx.getPackageName(), uri, StoredDirectoryHelper.PERMISSION_FLAGS);
|
||||||
|
|
||||||
|
StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(ctx, uri, null);
|
||||||
|
Log.i(TAG, "Acquiring tree success from " + uri.toString());
|
||||||
|
|
||||||
|
if (!mainStorage.canWrite())
|
||||||
|
throw new IOException("No write permissions on " + uri.toString());
|
||||||
|
} catch (IOException err) {
|
||||||
|
Log.e(TAG, "Error acquiring tree from " + uri.toString(), err);
|
||||||
|
showMessageDialog(R.string.general_error, R.string.no_available_dir);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
File target = Utils.getFileForUri(data.getData());
|
||||||
|
if (!target.canWrite()) {
|
||||||
|
showMessageDialog(R.string.download_to_sdcard_error_title, R.string.download_to_sdcard_error_message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uri = Uri.fromFile(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultPreferences.edit().putString(key, uri.toString()).apply();
|
||||||
|
updatePreferencesSummary();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
@ -108,19 +94,7 @@ public class NewPipeSettings {
|
|||||||
return new File(Environment.getExternalStorageDirectory(), defaultDirectoryName);
|
return new File(Environment.getExternalStorageDirectory(), defaultDirectoryName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void resetDownloadFolders(Context context) {
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void resetDownloadFolder(SharedPreferences prefs, String key, String defaultDirectoryName) {
|
|
||||||
SharedPreferences.Editor spEditor = prefs.edit();
|
|
||||||
spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName)));
|
|
||||||
spEditor.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getNewPipeChildFolderPathForDir(File dir) {
|
private static String getNewPipeChildFolderPathForDir(File dir) {
|
||||||
return new File(dir, "NewPipe").getAbsolutePath();
|
return new File(dir, "NewPipe").toURI().toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
package org.schabi.newpipe.streams;
|
package org.schabi.newpipe.streams;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
|
|
||||||
import java.io.EOFException;
|
import java.io.EOFException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import org.schabi.newpipe.streams.io.SharpStream;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author kapodamy
|
* @author kapodamy
|
||||||
@ -15,89 +16,239 @@ public class DataReader {
|
|||||||
public final static int INTEGER_SIZE = 4;
|
public final static int INTEGER_SIZE = 4;
|
||||||
public final static int FLOAT_SIZE = 4;
|
public final static int FLOAT_SIZE = 4;
|
||||||
|
|
||||||
private long pos;
|
private final static int BUFFER_SIZE = 128 * 1024;// 128 KiB
|
||||||
public final SharpStream stream;
|
|
||||||
private final boolean rewind;
|
private long position = 0;
|
||||||
|
private final SharpStream stream;
|
||||||
|
|
||||||
|
private InputStream view;
|
||||||
|
private int viewSize;
|
||||||
|
|
||||||
public DataReader(SharpStream stream) {
|
public DataReader(SharpStream stream) {
|
||||||
this.rewind = stream.canRewind();
|
|
||||||
this.stream = stream;
|
this.stream = stream;
|
||||||
this.pos = 0L;
|
this.readOffset = this.readBuffer.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long position() {
|
public long position() {
|
||||||
return pos;
|
return position;
|
||||||
}
|
}
|
||||||
|
|
||||||
public final int readInt() throws IOException {
|
public int read() throws IOException {
|
||||||
|
if (fillBuffer()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
position++;
|
||||||
|
readCount--;
|
||||||
|
|
||||||
|
return readBuffer[readOffset++] & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long skipBytes(long amount) throws IOException {
|
||||||
|
if (readCount < 0) {
|
||||||
|
return 0;
|
||||||
|
} else if (readCount == 0) {
|
||||||
|
amount = stream.skip(amount);
|
||||||
|
} else {
|
||||||
|
if (readCount > amount) {
|
||||||
|
readCount -= (int) amount;
|
||||||
|
readOffset += (int) amount;
|
||||||
|
} else {
|
||||||
|
amount = readCount + stream.skip(amount - readCount);
|
||||||
|
readCount = 0;
|
||||||
|
readOffset = readBuffer.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
position += amount;
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int readInt() throws IOException {
|
||||||
primitiveRead(INTEGER_SIZE);
|
primitiveRead(INTEGER_SIZE);
|
||||||
return primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3];
|
return primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3];
|
||||||
}
|
}
|
||||||
|
|
||||||
public final int read() throws IOException {
|
public short readShort() throws IOException {
|
||||||
int value = stream.read();
|
primitiveRead(SHORT_SIZE);
|
||||||
if (value == -1) {
|
return (short) (primitive[0] << 8 | primitive[1]);
|
||||||
throw new EOFException();
|
|
||||||
}
|
|
||||||
|
|
||||||
pos++;
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public final long skipBytes(long amount) throws IOException {
|
public long readLong() throws IOException {
|
||||||
amount = stream.skip(amount);
|
|
||||||
pos += amount;
|
|
||||||
return amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final long readLong() throws IOException {
|
|
||||||
primitiveRead(LONG_SIZE);
|
primitiveRead(LONG_SIZE);
|
||||||
long high = primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3];
|
long high = primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3];
|
||||||
long low = primitive[4] << 24 | primitive[5] << 16 | primitive[6] << 8 | primitive[7];
|
long low = primitive[4] << 24 | primitive[5] << 16 | primitive[6] << 8 | primitive[7];
|
||||||
return high << 32 | low;
|
return high << 32 | low;
|
||||||
}
|
}
|
||||||
|
|
||||||
public final short readShort() throws IOException {
|
public int read(byte[] buffer) throws IOException {
|
||||||
primitiveRead(SHORT_SIZE);
|
|
||||||
return (short) (primitive[0] << 8 | primitive[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public final int read(byte[] buffer) throws IOException {
|
|
||||||
return read(buffer, 0, buffer.length);
|
return read(buffer, 0, buffer.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
public final int read(byte[] buffer, int offset, int count) throws IOException {
|
public int read(byte[] buffer, int offset, int count) throws IOException {
|
||||||
int res = stream.read(buffer, offset, count);
|
if (readCount < 0) {
|
||||||
pos += res;
|
return -1;
|
||||||
|
}
|
||||||
|
int total = 0;
|
||||||
|
|
||||||
return res;
|
if (count >= readBuffer.length) {
|
||||||
|
if (readCount > 0) {
|
||||||
|
System.arraycopy(readBuffer, readOffset, buffer, offset, readCount);
|
||||||
|
readOffset += readCount;
|
||||||
|
|
||||||
|
offset += readCount;
|
||||||
|
count -= readCount;
|
||||||
|
|
||||||
|
total = readCount;
|
||||||
|
readCount = 0;
|
||||||
|
}
|
||||||
|
total += Math.max(stream.read(buffer, offset, count), 0);
|
||||||
|
} else {
|
||||||
|
while (count > 0 && !fillBuffer()) {
|
||||||
|
int read = Math.min(readCount, count);
|
||||||
|
System.arraycopy(readBuffer, readOffset, buffer, offset, read);
|
||||||
|
|
||||||
|
readOffset += read;
|
||||||
|
readCount -= read;
|
||||||
|
|
||||||
|
offset += read;
|
||||||
|
count -= read;
|
||||||
|
|
||||||
|
total += read;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
position += total;
|
||||||
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
public final boolean available() {
|
public boolean available() {
|
||||||
return stream.available() > 0;
|
return readCount > 0 || stream.available() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void rewind() throws IOException {
|
public void rewind() throws IOException {
|
||||||
stream.rewind();
|
stream.rewind();
|
||||||
pos = 0;
|
|
||||||
|
if ((position - viewSize) > 0) {
|
||||||
|
viewSize = 0;// drop view
|
||||||
|
} else {
|
||||||
|
viewSize += position;
|
||||||
|
}
|
||||||
|
|
||||||
|
position = 0;
|
||||||
|
readOffset = readBuffer.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean canRewind() {
|
public boolean canRewind() {
|
||||||
return rewind;
|
return stream.canRewind();
|
||||||
}
|
}
|
||||||
|
|
||||||
private short[] primitive = new short[LONG_SIZE];
|
/**
|
||||||
|
* Wraps this instance of {@code DataReader} into {@code InputStream}
|
||||||
|
* object. Note: Any read in the {@code DataReader} will not modify
|
||||||
|
* (decrease) the view size
|
||||||
|
*
|
||||||
|
* @param size the size of the view
|
||||||
|
* @return the view
|
||||||
|
*/
|
||||||
|
public InputStream getView(int size) {
|
||||||
|
if (view == null) {
|
||||||
|
view = new InputStream() {
|
||||||
|
@Override
|
||||||
|
public int read() throws IOException {
|
||||||
|
if (viewSize < 1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int res = DataReader.this.read();
|
||||||
|
if (res > 0) {
|
||||||
|
viewSize--;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] buffer) throws IOException {
|
||||||
|
return read(buffer, 0, buffer.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] buffer, int offset, int count) throws IOException {
|
||||||
|
if (viewSize < 1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int res = DataReader.this.read(buffer, offset, Math.min(viewSize, count));
|
||||||
|
viewSize -= res;
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long skip(long amount) throws IOException {
|
||||||
|
if (viewSize < 1) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int res = (int) DataReader.this.skipBytes(Math.min(amount, viewSize));
|
||||||
|
viewSize -= res;
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int available() {
|
||||||
|
return viewSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
viewSize = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean markSupported() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
viewSize = size;
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final short[] primitive = new short[LONG_SIZE];
|
||||||
|
|
||||||
private void primitiveRead(int amount) throws IOException {
|
private void primitiveRead(int amount) throws IOException {
|
||||||
byte[] buffer = new byte[amount];
|
byte[] buffer = new byte[amount];
|
||||||
int read = stream.read(buffer, 0, amount);
|
int read = read(buffer, 0, amount);
|
||||||
pos += read;
|
|
||||||
if (read != amount) {
|
if (read != amount) {
|
||||||
throw new EOFException("Truncated data, missing " + String.valueOf(amount - read) + " bytes");
|
throw new EOFException("Truncated stream, missing " + String.valueOf(amount - read) + " bytes");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < buffer.length; i++) {
|
for (int i = 0; i < amount; i++) {
|
||||||
primitive[i] = (short) (buffer[i] & 0xFF);// the "byte" datatype is signed and is very annoying
|
primitive[i] = (short) (buffer[i] & 0xFF);// the "byte" data type in java is signed and is very annoying
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final byte[] readBuffer = new byte[BUFFER_SIZE];
|
||||||
|
private int readOffset;
|
||||||
|
private int readCount;
|
||||||
|
|
||||||
|
private boolean fillBuffer() throws IOException {
|
||||||
|
if (readCount < 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (readOffset >= readBuffer.length) {
|
||||||
|
readCount = stream.read(readBuffer);
|
||||||
|
if (readCount < 1) {
|
||||||
|
readCount = -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
readOffset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return readCount < 1;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
package org.schabi.newpipe.streams;
|
package org.schabi.newpipe.streams;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
|
|
||||||
import java.io.EOFException;
|
import java.io.EOFException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
import org.schabi.newpipe.streams.io.SharpStream;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author kapodamy
|
* @author kapodamy
|
||||||
*/
|
*/
|
||||||
@ -35,14 +33,29 @@ public class Mp4DashReader {
|
|||||||
private static final int ATOM_TREX = 0x74726578;
|
private static final int ATOM_TREX = 0x74726578;
|
||||||
private static final int ATOM_TKHD = 0x746B6864;
|
private static final int ATOM_TKHD = 0x746B6864;
|
||||||
private static final int ATOM_MFRA = 0x6D667261;
|
private static final int ATOM_MFRA = 0x6D667261;
|
||||||
private static final int ATOM_TFRA = 0x74667261;
|
|
||||||
private static final int ATOM_MDHD = 0x6D646864;
|
private static final int ATOM_MDHD = 0x6D646864;
|
||||||
|
private static final int ATOM_EDTS = 0x65647473;
|
||||||
|
private static final int ATOM_ELST = 0x656C7374;
|
||||||
|
private static final int ATOM_HDLR = 0x68646C72;
|
||||||
|
private static final int ATOM_MINF = 0x6D696E66;
|
||||||
|
private static final int ATOM_DINF = 0x64696E66;
|
||||||
|
private static final int ATOM_STBL = 0x7374626C;
|
||||||
|
private static final int ATOM_STSD = 0x73747364;
|
||||||
|
private static final int ATOM_VMHD = 0x766D6864;
|
||||||
|
private static final int ATOM_SMHD = 0x736D6864;
|
||||||
|
|
||||||
private static final int BRAND_DASH = 0x64617368;
|
private static final int BRAND_DASH = 0x64617368;
|
||||||
|
private static final int BRAND_ISO5 = 0x69736F35;
|
||||||
|
|
||||||
|
private static final int HANDLER_VIDE = 0x76696465;
|
||||||
|
private static final int HANDLER_SOUN = 0x736F756E;
|
||||||
|
private static final int HANDLER_SUBT = 0x73756274;
|
||||||
// </editor-fold>
|
// </editor-fold>
|
||||||
|
|
||||||
private final DataReader stream;
|
private final DataReader stream;
|
||||||
|
|
||||||
private Mp4Track[] tracks = null;
|
private Mp4Track[] tracks = null;
|
||||||
|
private int[] brands = null;
|
||||||
|
|
||||||
private Box box;
|
private Box box;
|
||||||
private Moof moof;
|
private Moof moof;
|
||||||
@ -50,9 +63,10 @@ public class Mp4DashReader {
|
|||||||
private boolean chunkZero = false;
|
private boolean chunkZero = false;
|
||||||
|
|
||||||
private int selectedTrack = -1;
|
private int selectedTrack = -1;
|
||||||
|
private Box backupBox = null;
|
||||||
|
|
||||||
public enum TrackKind {
|
public enum TrackKind {
|
||||||
Audio, Video, Other
|
Audio, Video, Subtitles, Other
|
||||||
}
|
}
|
||||||
|
|
||||||
public Mp4DashReader(SharpStream source) {
|
public Mp4DashReader(SharpStream source) {
|
||||||
@ -65,8 +79,15 @@ public class Mp4DashReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
box = readBox(ATOM_FTYP);
|
box = readBox(ATOM_FTYP);
|
||||||
if (parse_ftyp() != BRAND_DASH) {
|
brands = parse_ftyp(box);
|
||||||
throw new NoSuchElementException("Main Brand is not dash");
|
switch (brands[0]) {
|
||||||
|
case BRAND_DASH:
|
||||||
|
case BRAND_ISO5:// ¿why not?
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new NoSuchElementException(
|
||||||
|
"Not a MPEG-4 DASH container, major brand is not 'dash' or 'iso5' is " + boxName(brands[0])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Moov moov = null;
|
Moov moov = null;
|
||||||
@ -84,8 +105,6 @@ public class Mp4DashReader {
|
|||||||
break;
|
break;
|
||||||
case ATOM_MFRA:
|
case ATOM_MFRA:
|
||||||
break;
|
break;
|
||||||
case ATOM_MDAT:
|
|
||||||
throw new IOException("Expected moof, found mdat");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,15 +126,26 @@ public class Mp4DashReader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (moov.trak[i].tkhd.bHeight == 0 && moov.trak[i].tkhd.bWidth == 0) {
|
switch (moov.trak[i].mdia.hdlr.subType) {
|
||||||
tracks[i].kind = moov.trak[i].tkhd.bVolume == 0 ? TrackKind.Other : TrackKind.Audio;
|
case HANDLER_VIDE:
|
||||||
} else {
|
tracks[i].kind = TrackKind.Video;
|
||||||
tracks[i].kind = TrackKind.Video;
|
break;
|
||||||
|
case HANDLER_SOUN:
|
||||||
|
tracks[i].kind = TrackKind.Audio;
|
||||||
|
break;
|
||||||
|
case HANDLER_SUBT:
|
||||||
|
tracks[i].kind = TrackKind.Subtitles;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
tracks[i].kind = TrackKind.Other;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
backupBox = box;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Mp4Track selectTrack(int index) {
|
Mp4Track selectTrack(int index) {
|
||||||
selectedTrack = index;
|
selectedTrack = index;
|
||||||
return tracks[index];
|
return tracks[index];
|
||||||
}
|
}
|
||||||
@ -126,7 +156,7 @@ public class Mp4DashReader {
|
|||||||
* @return list with a basic info
|
* @return list with a basic info
|
||||||
* @throws IOException if the source stream is not seekeable
|
* @throws IOException if the source stream is not seekeable
|
||||||
*/
|
*/
|
||||||
public int getFragmentsCount() throws IOException {
|
int getFragmentsCount() throws IOException {
|
||||||
if (selectedTrack < 0) {
|
if (selectedTrack < 0) {
|
||||||
throw new IllegalStateException("track no selected");
|
throw new IllegalStateException("track no selected");
|
||||||
}
|
}
|
||||||
@ -136,7 +166,6 @@ public class Mp4DashReader {
|
|||||||
|
|
||||||
Box tmp;
|
Box tmp;
|
||||||
int count = 0;
|
int count = 0;
|
||||||
long orig_offset = stream.position();
|
|
||||||
|
|
||||||
if (box.type == ATOM_MOOF) {
|
if (box.type == ATOM_MOOF) {
|
||||||
tmp = box;
|
tmp = box;
|
||||||
@ -162,17 +191,36 @@ public class Mp4DashReader {
|
|||||||
ensure(tmp);
|
ensure(tmp);
|
||||||
} while (stream.available() && (tmp = readBox()) != null);
|
} while (stream.available() && (tmp = readBox()) != null);
|
||||||
|
|
||||||
stream.rewind();
|
rewind();
|
||||||
stream.skipBytes((int) orig_offset);
|
|
||||||
|
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int[] getBrands() {
|
||||||
|
if (brands == null) throw new IllegalStateException("Not parsed");
|
||||||
|
return brands;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void rewind() throws IOException {
|
||||||
|
if (!stream.canRewind()) {
|
||||||
|
throw new IOException("The provided stream doesn't allow seek");
|
||||||
|
}
|
||||||
|
if (box == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
box = backupBox;
|
||||||
|
chunkZero = false;
|
||||||
|
|
||||||
|
stream.rewind();
|
||||||
|
stream.skipBytes(backupBox.offset + (DataReader.INTEGER_SIZE * 2));
|
||||||
|
}
|
||||||
|
|
||||||
public Mp4Track[] getAvailableTracks() {
|
public Mp4Track[] getAvailableTracks() {
|
||||||
return tracks;
|
return tracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Mp4TrackChunk getNextChunk() throws IOException {
|
public Mp4DashChunk getNextChunk(boolean infoOnly) throws IOException {
|
||||||
Mp4Track track = tracks[selectedTrack];
|
Mp4Track track = tracks[selectedTrack];
|
||||||
|
|
||||||
while (stream.available()) {
|
while (stream.available()) {
|
||||||
@ -208,7 +256,7 @@ public class Mp4DashReader {
|
|||||||
if (hasFlag(moof.traf.tfhd.bFlags, 0x10)) {
|
if (hasFlag(moof.traf.tfhd.bFlags, 0x10)) {
|
||||||
moof.traf.trun.chunkSize = moof.traf.tfhd.defaultSampleSize * moof.traf.trun.entryCount;
|
moof.traf.trun.chunkSize = moof.traf.tfhd.defaultSampleSize * moof.traf.trun.entryCount;
|
||||||
} else {
|
} else {
|
||||||
moof.traf.trun.chunkSize = box.size - 8;
|
moof.traf.trun.chunkSize = (int) (box.size - 8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!hasFlag(moof.traf.trun.bFlags, 0x900) && moof.traf.trun.chunkDuration == 0) {
|
if (!hasFlag(moof.traf.trun.bFlags, 0x900) && moof.traf.trun.chunkDuration == 0) {
|
||||||
@ -228,9 +276,12 @@ public class Mp4DashReader {
|
|||||||
continue;// find another chunk
|
continue;// find another chunk
|
||||||
}
|
}
|
||||||
|
|
||||||
Mp4TrackChunk chunk = new Mp4TrackChunk();
|
Mp4DashChunk chunk = new Mp4DashChunk();
|
||||||
chunk.moof = moof;
|
chunk.moof = moof;
|
||||||
chunk.data = new TrackDataChunk(stream, moof.traf.trun.chunkSize);
|
if (!infoOnly) {
|
||||||
|
chunk.data = stream.getView(moof.traf.trun.chunkSize);
|
||||||
|
}
|
||||||
|
|
||||||
moof = null;
|
moof = null;
|
||||||
|
|
||||||
stream.skipBytes(chunk.moof.traf.trun.dataOffset);
|
stream.skipBytes(chunk.moof.traf.trun.dataOffset);
|
||||||
@ -269,6 +320,10 @@ public class Mp4DashReader {
|
|||||||
b.size = stream.readInt();
|
b.size = stream.readInt();
|
||||||
b.type = stream.readInt();
|
b.type = stream.readInt();
|
||||||
|
|
||||||
|
if (b.size == 1) {
|
||||||
|
b.size = stream.readLong();
|
||||||
|
}
|
||||||
|
|
||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,6 +335,25 @@ public class Mp4DashReader {
|
|||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private byte[] readFullBox(Box ref) throws IOException {
|
||||||
|
// full box reading is limited to 2 GiB, and should be enough
|
||||||
|
int size = (int) ref.size;
|
||||||
|
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(size);
|
||||||
|
buffer.putInt(size);
|
||||||
|
buffer.putInt(ref.type);
|
||||||
|
|
||||||
|
int read = size - 8;
|
||||||
|
|
||||||
|
if (stream.read(buffer.array(), 8, read) != read) {
|
||||||
|
throw new EOFException(
|
||||||
|
String.format("EOF reached in box: type=%s offset=%s size=%s", boxName(ref.type), ref.offset, ref.size)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.array();
|
||||||
|
}
|
||||||
|
|
||||||
private void ensure(Box ref) throws IOException {
|
private void ensure(Box ref) throws IOException {
|
||||||
long skip = ref.offset + ref.size - stream.position();
|
long skip = ref.offset + ref.size - stream.position();
|
||||||
|
|
||||||
@ -310,6 +384,14 @@ public class Mp4DashReader {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Box untilAnyBox(Box ref) throws IOException {
|
||||||
|
if (stream.position() >= (ref.offset + ref.size)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return readBox();
|
||||||
|
}
|
||||||
|
|
||||||
// </editor-fold>
|
// </editor-fold>
|
||||||
|
|
||||||
// <editor-fold defaultState="collapsed" desc="Box readers">
|
// <editor-fold defaultState="collapsed" desc="Box readers">
|
||||||
@ -329,7 +411,7 @@ public class Mp4DashReader {
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -397,14 +479,14 @@ public class Mp4DashReader {
|
|||||||
|
|
||||||
private long parse_tfdt() throws IOException {
|
private long parse_tfdt() throws IOException {
|
||||||
int version = stream.read();
|
int version = stream.read();
|
||||||
stream.skipBytes(3);// flags
|
stream.skipBytes(3);// flags
|
||||||
return version == 0 ? readUint() : stream.readLong();
|
return version == 0 ? readUint() : stream.readLong();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Trun parse_trun() throws IOException {
|
private Trun parse_trun() throws IOException {
|
||||||
Trun obj = new Trun();
|
Trun obj = new Trun();
|
||||||
obj.bFlags = stream.readInt();
|
obj.bFlags = stream.readInt();
|
||||||
obj.entryCount = stream.readInt();// unsigned int
|
obj.entryCount = stream.readInt();// unsigned int
|
||||||
|
|
||||||
obj.entries_rowSize = 0;
|
obj.entries_rowSize = 0;
|
||||||
if (hasFlag(obj.bFlags, 0x0100)) {
|
if (hasFlag(obj.bFlags, 0x0100)) {
|
||||||
@ -448,11 +530,18 @@ public class Mp4DashReader {
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int parse_ftyp() throws IOException {
|
private int[] parse_ftyp(Box ref) throws IOException {
|
||||||
int brand = stream.readInt();
|
int i = 0;
|
||||||
|
int[] list = new int[(int) ((ref.offset + ref.size - stream.position() - 4) / 4)];
|
||||||
|
|
||||||
|
list[i++] = stream.readInt();// major brand
|
||||||
|
|
||||||
stream.skipBytes(4);// minor version
|
stream.skipBytes(4);// minor version
|
||||||
|
|
||||||
return brand;
|
for (; i < list.length; i++)
|
||||||
|
list[i] = stream.readInt();// compatible brands
|
||||||
|
|
||||||
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mvhd parse_mvhd() throws IOException {
|
private Mvhd parse_mvhd() throws IOException {
|
||||||
@ -521,32 +610,66 @@ public class Mp4DashReader {
|
|||||||
trak.tkhd = parse_tkhd();
|
trak.tkhd = parse_tkhd();
|
||||||
ensure(b);
|
ensure(b);
|
||||||
|
|
||||||
b = untilBox(ref, ATOM_MDIA);
|
while ((b = untilBox(ref, ATOM_MDIA, ATOM_EDTS)) != null) {
|
||||||
trak.mdia = new byte[b.size];
|
switch (b.type) {
|
||||||
|
case ATOM_MDIA:
|
||||||
|
trak.mdia = parse_mdia(b);
|
||||||
|
break;
|
||||||
|
case ATOM_EDTS:
|
||||||
|
trak.edst_elst = parse_edts(b);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
ByteBuffer buffer = ByteBuffer.wrap(trak.mdia);
|
ensure(b);
|
||||||
buffer.putInt(b.size);
|
}
|
||||||
buffer.putInt(ATOM_MDIA);
|
|
||||||
stream.read(trak.mdia, 8, b.size - 8);
|
|
||||||
|
|
||||||
trak.mdia_mdhd_timeScale = parse_mdia(buffer);
|
|
||||||
|
|
||||||
return trak;
|
return trak;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int parse_mdia(ByteBuffer data) {
|
private Mdia parse_mdia(Box ref) throws IOException {
|
||||||
while (data.hasRemaining()) {
|
Mdia obj = new Mdia();
|
||||||
int end = data.position() + data.getInt();
|
|
||||||
if (data.getInt() == ATOM_MDHD) {
|
|
||||||
byte version = data.get();
|
|
||||||
data.position(data.position() + 3 + ((version == 0 ? 4 : 8) * 2));
|
|
||||||
return data.getInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
data.position(end);
|
Box b;
|
||||||
|
while ((b = untilBox(ref, ATOM_MDHD, ATOM_HDLR, ATOM_MINF)) != null) {
|
||||||
|
switch (b.type) {
|
||||||
|
case ATOM_MDHD:
|
||||||
|
obj.mdhd = readFullBox(b);
|
||||||
|
|
||||||
|
// read time scale
|
||||||
|
ByteBuffer buffer = ByteBuffer.wrap(obj.mdhd);
|
||||||
|
byte version = buffer.get(8);
|
||||||
|
buffer.position(12 + ((version == 0 ? 4 : 8) * 2));
|
||||||
|
obj.mdhd_timeScale = buffer.getInt();
|
||||||
|
break;
|
||||||
|
case ATOM_HDLR:
|
||||||
|
obj.hdlr = parse_hdlr(b);
|
||||||
|
break;
|
||||||
|
case ATOM_MINF:
|
||||||
|
obj.minf = parse_minf(b);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ensure(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;// this NEVER should happen
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Hdlr parse_hdlr(Box ref) throws IOException {
|
||||||
|
// version
|
||||||
|
// flags
|
||||||
|
stream.skipBytes(4);
|
||||||
|
|
||||||
|
Hdlr obj = new Hdlr();
|
||||||
|
obj.bReserved = new byte[12];
|
||||||
|
|
||||||
|
obj.type = stream.readInt();
|
||||||
|
obj.subType = stream.readInt();
|
||||||
|
stream.read(obj.bReserved);
|
||||||
|
|
||||||
|
// component name (is a ansi/ascii string)
|
||||||
|
stream.skipBytes((ref.offset + ref.size) - stream.position());
|
||||||
|
|
||||||
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Moov parse_moov(Box ref) throws IOException {
|
private Moov parse_moov(Box ref) throws IOException {
|
||||||
@ -570,7 +693,7 @@ public class Mp4DashReader {
|
|||||||
ensure(b);
|
ensure(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
moov.trak = tmp.toArray(new Trak[tmp.size()]);
|
moov.trak = tmp.toArray(new Trak[0]);
|
||||||
|
|
||||||
return moov;
|
return moov;
|
||||||
}
|
}
|
||||||
@ -584,7 +707,7 @@ public class Mp4DashReader {
|
|||||||
ensure(b);
|
ensure(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
return tmp.toArray(new Trex[tmp.size()]);
|
return tmp.toArray(new Trex[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Trex parse_trex() throws IOException {
|
private Trex parse_trex() throws IOException {
|
||||||
@ -602,74 +725,74 @@ public class Mp4DashReader {
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Tfra parse_tfra() throws IOException {
|
private Elst parse_edts(Box ref) throws IOException {
|
||||||
int version = stream.read();
|
Box b = untilBox(ref, ATOM_ELST);
|
||||||
|
if (b == null) {
|
||||||
stream.skipBytes(3);// flags
|
return null;
|
||||||
|
|
||||||
Tfra tfra = new Tfra();
|
|
||||||
tfra.trackId = stream.readInt();
|
|
||||||
|
|
||||||
stream.skipBytes(3);// reserved
|
|
||||||
int bFlags = stream.read();
|
|
||||||
int size_tts = ((bFlags >> 4) & 3) + ((bFlags >> 2) & 3) + (bFlags & 3);
|
|
||||||
|
|
||||||
tfra.entries_time = new int[stream.readInt()];
|
|
||||||
|
|
||||||
for (int i = 0; i < tfra.entries_time.length; i++) {
|
|
||||||
tfra.entries_time[i] = version == 0 ? stream.readInt() : (int) stream.readLong();
|
|
||||||
stream.skipBytes(size_tts + (version == 0 ? 4 : 8));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return tfra;
|
Elst obj = new Elst();
|
||||||
}
|
|
||||||
|
|
||||||
private Sidx parse_sidx() throws IOException {
|
|
||||||
int version = stream.read();
|
|
||||||
|
|
||||||
|
boolean v1 = stream.read() == 1;
|
||||||
stream.skipBytes(3);// flags
|
stream.skipBytes(3);// flags
|
||||||
|
|
||||||
Sidx obj = new Sidx();
|
int entryCount = stream.readInt();
|
||||||
obj.referenceId = stream.readInt();
|
if (entryCount < 1) {
|
||||||
obj.timescale = stream.readInt();
|
obj.bMediaRate = 0x00010000;// default media rate (1.0)
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
// earliest presentation entries_time
|
if (v1) {
|
||||||
// first offset
|
stream.skipBytes(DataReader.LONG_SIZE);// segment duration
|
||||||
// reserved
|
obj.MediaTime = stream.readLong();
|
||||||
stream.skipBytes((2 * (version == 0 ? 4 : 8)) + 2);
|
// ignore all remain entries
|
||||||
|
stream.skipBytes((entryCount - 1) * (DataReader.LONG_SIZE * 2));
|
||||||
|
} else {
|
||||||
|
stream.skipBytes(DataReader.INTEGER_SIZE);// segment duration
|
||||||
|
obj.MediaTime = stream.readInt();
|
||||||
|
}
|
||||||
|
|
||||||
obj.entries_subsegmentDuration = new int[stream.readShort()];
|
obj.bMediaRate = stream.readInt();
|
||||||
|
|
||||||
for (int i = 0; i < obj.entries_subsegmentDuration.length; i++) {
|
return obj;
|
||||||
// reference type
|
}
|
||||||
// referenced size
|
|
||||||
stream.skipBytes(4);
|
|
||||||
obj.entries_subsegmentDuration[i] = stream.readInt();// unsigned int
|
|
||||||
|
|
||||||
// starts with SAP
|
private Minf parse_minf(Box ref) throws IOException {
|
||||||
// SAP type
|
Minf obj = new Minf();
|
||||||
// SAP delta entries_time
|
|
||||||
stream.skipBytes(4);
|
Box b;
|
||||||
|
while ((b = untilAnyBox(ref)) != null) {
|
||||||
|
|
||||||
|
switch (b.type) {
|
||||||
|
case ATOM_DINF:
|
||||||
|
obj.dinf = readFullBox(b);
|
||||||
|
break;
|
||||||
|
case ATOM_STBL:
|
||||||
|
obj.stbl_stsd = parse_stbl(b);
|
||||||
|
break;
|
||||||
|
case ATOM_VMHD:
|
||||||
|
case ATOM_SMHD:
|
||||||
|
obj.$mhd = readFullBox(b);
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
ensure(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Tfra[] parse_mfra(Box ref, int trackCount) throws IOException {
|
/**
|
||||||
ArrayList<Tfra> tmp = new ArrayList<>(trackCount);
|
* this only read the "stsd" box inside
|
||||||
long limit = ref.offset + ref.size;
|
*/
|
||||||
|
private byte[] parse_stbl(Box ref) throws IOException {
|
||||||
|
Box b = untilBox(ref, ATOM_STSD);
|
||||||
|
|
||||||
while (stream.position() < limit) {
|
if (b == null) {
|
||||||
box = readBox();
|
return new byte[0];// this never should happens (missing codec startup data)
|
||||||
|
|
||||||
if (box.type == ATOM_TFRA) {
|
|
||||||
tmp.add(parse_tfra());
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure(box);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return tmp.toArray(new Tfra[tmp.size()]);
|
return readFullBox(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
// </editor-fold>
|
// </editor-fold>
|
||||||
@ -679,14 +802,7 @@ public class Mp4DashReader {
|
|||||||
|
|
||||||
int type;
|
int type;
|
||||||
long offset;
|
long offset;
|
||||||
int size;
|
long size;
|
||||||
}
|
|
||||||
|
|
||||||
class Sidx {
|
|
||||||
|
|
||||||
int timescale;
|
|
||||||
int referenceId;
|
|
||||||
int[] entries_subsegmentDuration;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Moof {
|
public class Moof {
|
||||||
@ -711,12 +827,16 @@ public class Mp4DashReader {
|
|||||||
int defaultSampleFlags;
|
int defaultSampleFlags;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TrunEntry {
|
class TrunEntry {
|
||||||
|
|
||||||
|
int sampleDuration;
|
||||||
|
int sampleSize;
|
||||||
|
int sampleFlags;
|
||||||
|
int sampleCompositionTimeOffset;
|
||||||
|
|
||||||
|
boolean hasCompositionTimeOffset;
|
||||||
|
boolean isKeyframe;
|
||||||
|
|
||||||
public int sampleDuration;
|
|
||||||
public int sampleSize;
|
|
||||||
public int sampleFlags;
|
|
||||||
public int sampleCompositionTimeOffset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Trun {
|
public class Trun {
|
||||||
@ -749,6 +869,31 @@ public class Mp4DashReader {
|
|||||||
entry.sampleCompositionTimeOffset = buffer.getInt();
|
entry.sampleCompositionTimeOffset = buffer.getInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry.hasCompositionTimeOffset = hasFlag(bFlags, 0x0800);
|
||||||
|
entry.isKeyframe = !hasFlag(entry.sampleFlags, 0x10000);
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TrunEntry getAbsoluteEntry(int i, Tfhd header) {
|
||||||
|
TrunEntry entry = getEntry(i);
|
||||||
|
|
||||||
|
if (!hasFlag(bFlags, 0x0100) && hasFlag(header.bFlags, 0x20)) {
|
||||||
|
entry.sampleFlags = header.defaultSampleFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasFlag(bFlags, 0x0200) && hasFlag(header.bFlags, 0x10)) {
|
||||||
|
entry.sampleSize = header.defaultSampleSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasFlag(bFlags, 0x0100) && hasFlag(header.bFlags, 0x08)) {
|
||||||
|
entry.sampleDuration = header.defaultSampleDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i == 0 && hasFlag(bFlags, 0x0004)) {
|
||||||
|
entry.sampleFlags = bFirstSampleFlags;
|
||||||
|
}
|
||||||
|
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -768,9 +913,9 @@ public class Mp4DashReader {
|
|||||||
public class Trak {
|
public class Trak {
|
||||||
|
|
||||||
public Tkhd tkhd;
|
public Tkhd tkhd;
|
||||||
public int mdia_mdhd_timeScale;
|
public Elst edst_elst;
|
||||||
|
public Mdia mdia;
|
||||||
|
|
||||||
byte[] mdia;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Mvhd {
|
class Mvhd {
|
||||||
@ -786,12 +931,6 @@ public class Mp4DashReader {
|
|||||||
Trex[] mvex_trex;
|
Trex[] mvex_trex;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Tfra {
|
|
||||||
|
|
||||||
int trackId;
|
|
||||||
int[] entries_time;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Trex {
|
public class Trex {
|
||||||
|
|
||||||
private int trackId;
|
private int trackId;
|
||||||
@ -801,6 +940,34 @@ public class Mp4DashReader {
|
|||||||
int defaultSampleFlags;
|
int defaultSampleFlags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class Elst {
|
||||||
|
|
||||||
|
public long MediaTime;
|
||||||
|
public int bMediaRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Mdia {
|
||||||
|
|
||||||
|
public int mdhd_timeScale;
|
||||||
|
public byte[] mdhd;
|
||||||
|
public Hdlr hdlr;
|
||||||
|
public Minf minf;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Hdlr {
|
||||||
|
|
||||||
|
public int type;
|
||||||
|
public int subType;
|
||||||
|
public byte[] bReserved;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Minf {
|
||||||
|
|
||||||
|
public byte[] dinf;
|
||||||
|
public byte[] stbl_stsd;
|
||||||
|
public byte[] $mhd;
|
||||||
|
}
|
||||||
|
|
||||||
public class Mp4Track {
|
public class Mp4Track {
|
||||||
|
|
||||||
public TrackKind kind;
|
public TrackKind kind;
|
||||||
@ -808,10 +975,43 @@ public class Mp4DashReader {
|
|||||||
public Trex trex;
|
public Trex trex;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Mp4TrackChunk {
|
public class Mp4DashChunk {
|
||||||
|
|
||||||
public InputStream data;
|
public InputStream data;
|
||||||
public Moof moof;
|
public Moof moof;
|
||||||
|
private int i = 0;
|
||||||
|
|
||||||
|
public TrunEntry getNextSampleInfo() {
|
||||||
|
if (i >= moof.traf.trun.entryCount) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return moof.traf.trun.getAbsoluteEntry(i++, moof.traf.tfhd);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mp4DashSample getNextSample() throws IOException {
|
||||||
|
if (data == null) {
|
||||||
|
throw new IllegalStateException("This chunk has info only");
|
||||||
|
}
|
||||||
|
if (i >= moof.traf.trun.entryCount) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Mp4DashSample sample = new Mp4DashSample();
|
||||||
|
sample.info = moof.traf.trun.getAbsoluteEntry(i++, moof.traf.tfhd);
|
||||||
|
sample.data = new byte[sample.info.sampleSize];
|
||||||
|
|
||||||
|
if (data.read(sample.data) != sample.info.sampleSize) {
|
||||||
|
throw new EOFException("EOF reached while reading a sample");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sample;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Mp4DashSample {
|
||||||
|
|
||||||
|
public TrunEntry info;
|
||||||
|
public byte[] data;
|
||||||
}
|
}
|
||||||
//</editor-fold>
|
//</editor-fold>
|
||||||
}
|
}
|
||||||
|
@ -1,623 +0,0 @@
|
|||||||
package org.schabi.newpipe.streams;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.streams.io.SharpStream;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.streams.Mp4DashReader.Mp4Track;
|
|
||||||
import org.schabi.newpipe.streams.Mp4DashReader.Mp4TrackChunk;
|
|
||||||
import org.schabi.newpipe.streams.Mp4DashReader.Trak;
|
|
||||||
import org.schabi.newpipe.streams.Mp4DashReader.Trex;
|
|
||||||
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.streams.Mp4DashReader.hasFlag;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @author kapodamy
|
|
||||||
*/
|
|
||||||
public class Mp4DashWriter {
|
|
||||||
|
|
||||||
private final static byte DIMENSIONAL_FIVE = 5;
|
|
||||||
private final static byte DIMENSIONAL_TWO = 2;
|
|
||||||
private final static short DEFAULT_TIMESCALE = 1000;
|
|
||||||
private final static int BUFFER_SIZE = 8 * 1024;
|
|
||||||
private final static byte DEFAULT_TREX_SIZE = 32;
|
|
||||||
private final static byte[] TFRA_TTS_DEFAULT = new byte[]{0x01, 0x01, 0x01};
|
|
||||||
private final static int EPOCH_OFFSET = 2082844800;
|
|
||||||
|
|
||||||
private Mp4Track[] infoTracks;
|
|
||||||
private SharpStream[] sourceTracks;
|
|
||||||
|
|
||||||
private Mp4DashReader[] readers;
|
|
||||||
private final long time;
|
|
||||||
|
|
||||||
private boolean done = false;
|
|
||||||
private boolean parsed = false;
|
|
||||||
|
|
||||||
private long written = 0;
|
|
||||||
private ArrayList<ArrayList<Integer>> chunkTimes;
|
|
||||||
private ArrayList<Long> moofOffsets;
|
|
||||||
private ArrayList<Integer> fragSizes;
|
|
||||||
|
|
||||||
public Mp4DashWriter(SharpStream... source) {
|
|
||||||
sourceTracks = source;
|
|
||||||
readers = new Mp4DashReader[sourceTracks.length];
|
|
||||||
infoTracks = new Mp4Track[sourceTracks.length];
|
|
||||||
time = (System.currentTimeMillis() / 1000L) + EPOCH_OFFSET;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Mp4Track[] getTracksFromSource(int sourceIndex) throws IllegalStateException {
|
|
||||||
if (!parsed) {
|
|
||||||
throw new IllegalStateException("All sources must be parsed first");
|
|
||||||
}
|
|
||||||
|
|
||||||
return readers[sourceIndex].getAvailableTracks();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void parseSources() throws IOException, IllegalStateException {
|
|
||||||
if (done) {
|
|
||||||
throw new IllegalStateException("already done");
|
|
||||||
}
|
|
||||||
if (parsed) {
|
|
||||||
throw new IllegalStateException("already parsed");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (int i = 0; i < readers.length; i++) {
|
|
||||||
readers[i] = new Mp4DashReader(sourceTracks[i]);
|
|
||||||
readers[i].parse();
|
|
||||||
}
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
parsed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void selectTracks(int... trackIndex) throws IOException {
|
|
||||||
if (done) {
|
|
||||||
throw new IOException("already done");
|
|
||||||
}
|
|
||||||
if (chunkTimes != null) {
|
|
||||||
throw new IOException("tracks already selected");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
chunkTimes = new ArrayList<>(readers.length);
|
|
||||||
moofOffsets = new ArrayList<>(32);
|
|
||||||
fragSizes = new ArrayList<>(32);
|
|
||||||
|
|
||||||
for (int i = 0; i < readers.length; i++) {
|
|
||||||
infoTracks[i] = readers[i].selectTrack(trackIndex[i]);
|
|
||||||
|
|
||||||
chunkTimes.add(new ArrayList<Integer>(32));
|
|
||||||
}
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
parsed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getBytesWritten() {
|
|
||||||
return written;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void build(SharpStream out) throws IOException, RuntimeException {
|
|
||||||
if (done) {
|
|
||||||
throw new RuntimeException("already done");
|
|
||||||
}
|
|
||||||
if (!out.canWrite()) {
|
|
||||||
throw new IOException("the provided output is not writable");
|
|
||||||
}
|
|
||||||
|
|
||||||
long sidxOffsets = -1;
|
|
||||||
int maxFrags = 0;
|
|
||||||
|
|
||||||
for (SharpStream stream : sourceTracks) {
|
|
||||||
if (!stream.canRewind()) {
|
|
||||||
sidxOffsets = -2;// sidx not available
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
dump(make_ftyp(), out);
|
|
||||||
dump(make_moov(), out);
|
|
||||||
|
|
||||||
if (sidxOffsets == -1 && out.canRewind()) {
|
|
||||||
//<editor-fold defaultstate="collapsed" desc="calculate sidx">
|
|
||||||
int reserved = 0;
|
|
||||||
for (Mp4DashReader reader : readers) {
|
|
||||||
int count = reader.getFragmentsCount();
|
|
||||||
if (count > maxFrags) {
|
|
||||||
maxFrags = count;
|
|
||||||
}
|
|
||||||
reserved += 12 + calcSidxBodySize(count);
|
|
||||||
}
|
|
||||||
if (maxFrags > 0xFFFF) {
|
|
||||||
sidxOffsets = -3;// TODO: to many fragments, needs a multi-sidx implementation
|
|
||||||
} else {
|
|
||||||
sidxOffsets = written;
|
|
||||||
dump(make_free(reserved), out);
|
|
||||||
}
|
|
||||||
//</editor-fold>
|
|
||||||
}
|
|
||||||
ArrayList<Mp4TrackChunk> chunks = new ArrayList<>(readers.length);
|
|
||||||
chunks.add(null);
|
|
||||||
|
|
||||||
int read;
|
|
||||||
byte[] buffer = new byte[BUFFER_SIZE];
|
|
||||||
int sequenceNumber = 1;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
chunks.clear();
|
|
||||||
|
|
||||||
for (int i = 0; i < readers.length; i++) {
|
|
||||||
Mp4TrackChunk chunk = readers[i].getNextChunk();
|
|
||||||
if (chunk == null || chunk.moof.traf.trun.chunkSize < 1) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
chunk.moof.traf.tfhd.trackId = i + 1;
|
|
||||||
chunks.add(chunk);
|
|
||||||
|
|
||||||
if (sequenceNumber == 1) {
|
|
||||||
if (chunk.moof.traf.trun.entryCount > 0 && hasFlag(chunk.moof.traf.trun.bFlags, 0x0800)) {
|
|
||||||
chunkTimes.get(i).add(chunk.moof.traf.trun.getEntry(0).sampleCompositionTimeOffset);
|
|
||||||
} else {
|
|
||||||
chunkTimes.get(i).add(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
chunkTimes.get(i).add(chunk.moof.traf.trun.chunkDuration);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chunks.size() < 1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
long offset = written;
|
|
||||||
moofOffsets.add(offset);
|
|
||||||
|
|
||||||
dump(make_moof(sequenceNumber++, chunks, offset), out);
|
|
||||||
dump(make_mdat(chunks), out);
|
|
||||||
|
|
||||||
for (Mp4TrackChunk chunk : chunks) {
|
|
||||||
while ((read = chunk.data.read(buffer)) > 0) {
|
|
||||||
out.write(buffer, 0, read);
|
|
||||||
written += read;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fragSizes.add((int) (written - offset));
|
|
||||||
}
|
|
||||||
|
|
||||||
dump(make_mfra(), out);
|
|
||||||
|
|
||||||
if (sidxOffsets > 0 && moofOffsets.size() == maxFrags) {
|
|
||||||
long len = written;
|
|
||||||
|
|
||||||
out.rewind();
|
|
||||||
out.skip(sidxOffsets);
|
|
||||||
|
|
||||||
written = sidxOffsets;
|
|
||||||
sidxOffsets = moofOffsets.get(0);
|
|
||||||
|
|
||||||
for (int i = 0; i < readers.length; i++) {
|
|
||||||
dump(make_sidx(i, sidxOffsets - written), out);
|
|
||||||
}
|
|
||||||
|
|
||||||
written = len;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
done = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isDone() {
|
|
||||||
return done;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isParsed() {
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void close() {
|
|
||||||
done = true;
|
|
||||||
parsed = true;
|
|
||||||
|
|
||||||
for (SharpStream src : sourceTracks) {
|
|
||||||
src.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceTracks = null;
|
|
||||||
readers = null;
|
|
||||||
infoTracks = null;
|
|
||||||
moofOffsets = null;
|
|
||||||
chunkTimes = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// <editor-fold defaultstate="collapsed" desc="Utils">
|
|
||||||
private void dump(byte[][] buffer, SharpStream stream) throws IOException {
|
|
||||||
for (byte[] buff : buffer) {
|
|
||||||
stream.write(buff);
|
|
||||||
written += buff.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[][] lengthFor(byte[][] buffer) {
|
|
||||||
int length = 0;
|
|
||||||
for (byte[] buff : buffer) {
|
|
||||||
length += buff.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
ByteBuffer.wrap(buffer[0]).putInt(length);
|
|
||||||
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int calcSidxBodySize(int entryCount) {
|
|
||||||
return 4 + 4 + 8 + 8 + 4 + (entryCount * 12);
|
|
||||||
}
|
|
||||||
// </editor-fold>
|
|
||||||
|
|
||||||
// <editor-fold defaultstate="collapsed" desc="Box makers">
|
|
||||||
private byte[][] make_moof(int sequence, ArrayList<Mp4TrackChunk> chunks, long referenceOffset) {
|
|
||||||
int pos = 2;
|
|
||||||
TrunExtra[] extra = new TrunExtra[chunks.size()];
|
|
||||||
|
|
||||||
byte[][] buffer = new byte[pos + (extra.length * DIMENSIONAL_FIVE)][];
|
|
||||||
buffer[0] = new byte[]{
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x6F, 0x66,// info header
|
|
||||||
0x00, 0x00, 0x00, 0x10, 0x6D, 0x66, 0x68, 0x64, 0x00, 0x00, 0x00, 0x00//mfhd
|
|
||||||
};
|
|
||||||
buffer[1] = new byte[4];
|
|
||||||
ByteBuffer.wrap(buffer[1]).putInt(sequence);
|
|
||||||
|
|
||||||
for (int i = 0; i < extra.length; i++) {
|
|
||||||
extra[i] = new TrunExtra();
|
|
||||||
for (byte[] buff : make_traf(chunks.get(i), extra[i], referenceOffset)) {
|
|
||||||
buffer[pos++] = buff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lengthFor(buffer);
|
|
||||||
|
|
||||||
int offset = 8 + ByteBuffer.wrap(buffer[0]).getInt();
|
|
||||||
|
|
||||||
for (int i = 0; i < extra.length; i++) {
|
|
||||||
extra[i].byteBuffer.putInt(offset);
|
|
||||||
offset += chunks.get(i).moof.traf.trun.chunkSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[][] make_traf(Mp4TrackChunk chunk, TrunExtra extra, long moofOffset) {
|
|
||||||
byte[][] buffer = new byte[DIMENSIONAL_FIVE][];
|
|
||||||
buffer[0] = new byte[]{
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x66,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x74, 0x66, 0x68, 0x64
|
|
||||||
};
|
|
||||||
|
|
||||||
int flags = (chunk.moof.traf.tfhd.bFlags & 0x38) | 0x01;
|
|
||||||
byte tfhdBodySize = 8 + 8;
|
|
||||||
if (hasFlag(flags, 0x08)) {
|
|
||||||
tfhdBodySize += 4;
|
|
||||||
}
|
|
||||||
if (hasFlag(flags, 0x10)) {
|
|
||||||
tfhdBodySize += 4;
|
|
||||||
}
|
|
||||||
if (hasFlag(flags, 0x20)) {
|
|
||||||
tfhdBodySize += 4;
|
|
||||||
}
|
|
||||||
buffer[1] = new byte[tfhdBodySize];
|
|
||||||
ByteBuffer set = ByteBuffer.wrap(buffer[1]);
|
|
||||||
set.position(4);
|
|
||||||
set.putInt(chunk.moof.traf.tfhd.trackId);
|
|
||||||
set.putLong(moofOffset);
|
|
||||||
if (hasFlag(flags, 0x08)) {
|
|
||||||
set.putInt(chunk.moof.traf.tfhd.defaultSampleDuration);
|
|
||||||
}
|
|
||||||
if (hasFlag(flags, 0x10)) {
|
|
||||||
set.putInt(chunk.moof.traf.tfhd.defaultSampleSize);
|
|
||||||
}
|
|
||||||
if (hasFlag(flags, 0x20)) {
|
|
||||||
set.putInt(chunk.moof.traf.tfhd.defaultSampleFlags);
|
|
||||||
}
|
|
||||||
set.putInt(0, flags);
|
|
||||||
ByteBuffer.wrap(buffer[0]).putInt(8, 8 + tfhdBodySize);
|
|
||||||
|
|
||||||
buffer[2] = new byte[]{
|
|
||||||
0x00, 0x00, 0x00, 0x14,
|
|
||||||
0x74, 0x66, 0x64, 0x74,
|
|
||||||
0x01, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00
|
|
||||||
};
|
|
||||||
|
|
||||||
ByteBuffer.wrap(buffer[2]).putLong(12, chunk.moof.traf.tfdt);
|
|
||||||
|
|
||||||
buffer[3] = new byte[]{
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x75, 0x6E,
|
|
||||||
0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00
|
|
||||||
};
|
|
||||||
|
|
||||||
buffer[4] = chunk.moof.traf.trun.bEntries;
|
|
||||||
|
|
||||||
lengthFor(buffer);
|
|
||||||
|
|
||||||
set = ByteBuffer.wrap(buffer[3]);
|
|
||||||
set.putInt(buffer[3].length + buffer[4].length);
|
|
||||||
set.position(8);
|
|
||||||
set.putInt((chunk.moof.traf.trun.bFlags | 0x01) & 0x0F01);
|
|
||||||
set.putInt(chunk.moof.traf.trun.entryCount);
|
|
||||||
extra.byteBuffer = set;
|
|
||||||
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[][] make_mdat(ArrayList<Mp4TrackChunk> chunks) {
|
|
||||||
byte[][] buffer = new byte[][]{
|
|
||||||
{
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x61, 0x74
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
int length = 0;
|
|
||||||
|
|
||||||
for (Mp4TrackChunk chunk : chunks) {
|
|
||||||
length += chunk.moof.traf.trun.chunkSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
ByteBuffer.wrap(buffer[0]).putInt(length + 8);
|
|
||||||
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[][] make_ftyp() {
|
|
||||||
return new byte[][]{
|
|
||||||
{
|
|
||||||
0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x64, 0x61, 0x73, 0x68, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x6D, 0x70, 0x34, 0x31, 0x69, 0x73, 0x6F, 0x6D, 0x69, 0x73, 0x6F, 0x36, 0x69, 0x73, 0x6F, 0x32
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[][] make_mvhd() {
|
|
||||||
byte[][] buffer = new byte[DIMENSIONAL_FIVE][];
|
|
||||||
|
|
||||||
buffer[0] = new byte[]{
|
|
||||||
0x00, 0x00, 0x00, 0x78, 0x6D, 0x76, 0x68, 0x64, 0x01, 0x00, 0x00, 0x00
|
|
||||||
};
|
|
||||||
buffer[1] = new byte[28];
|
|
||||||
buffer[2] = new byte[]{
|
|
||||||
0x00, 0x01, 0x00, 0x00, 0x01, 0x00,// default volume and rate
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,// reserved values
|
|
||||||
// default matrix
|
|
||||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x40, 0x00, 0x00, 0x00
|
|
||||||
};
|
|
||||||
buffer[3] = new byte[24];// predefined
|
|
||||||
buffer[4] = ByteBuffer.allocate(4).putInt(infoTracks.length + 1).array();
|
|
||||||
|
|
||||||
long longestTrack = 0;
|
|
||||||
|
|
||||||
for (Mp4Track track : infoTracks) {
|
|
||||||
long tmp = (long) ((track.trak.tkhd.duration / (double) track.trak.mdia_mdhd_timeScale) * DEFAULT_TIMESCALE);
|
|
||||||
if (tmp > longestTrack) {
|
|
||||||
longestTrack = tmp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ByteBuffer.wrap(buffer[1])
|
|
||||||
.putLong(time)
|
|
||||||
.putLong(time)
|
|
||||||
.putInt(DEFAULT_TIMESCALE)
|
|
||||||
.putLong(longestTrack);
|
|
||||||
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[][] make_trak(int trackId, Trak trak) throws RuntimeException {
|
|
||||||
if (trak.tkhd.matrix.length != 36) {
|
|
||||||
throw new RuntimeException("bad track matrix length (expected 36)");
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[][] buffer = new byte[DIMENSIONAL_FIVE][];
|
|
||||||
|
|
||||||
buffer[0] = new byte[]{
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x6B,// trak header
|
|
||||||
0x00, 0x00, 0x00, 0x68, 0x74, 0x6B, 0x68, 0x64, 0x01, 0x00, 0x00, 0x03 // tkhd header
|
|
||||||
};
|
|
||||||
buffer[1] = new byte[48];
|
|
||||||
buffer[2] = trak.tkhd.matrix;
|
|
||||||
buffer[3] = new byte[8];
|
|
||||||
buffer[4] = trak.mdia;
|
|
||||||
|
|
||||||
ByteBuffer set = ByteBuffer.wrap(buffer[1]);
|
|
||||||
set.putLong(time);
|
|
||||||
set.putLong(time);
|
|
||||||
set.putInt(trackId);
|
|
||||||
set.position(24);
|
|
||||||
set.putLong(trak.tkhd.duration);
|
|
||||||
set.position(40);
|
|
||||||
set.putShort(trak.tkhd.bLayer);
|
|
||||||
set.putShort(trak.tkhd.bAlternateGroup);
|
|
||||||
set.putShort(trak.tkhd.bVolume);
|
|
||||||
|
|
||||||
ByteBuffer.wrap(buffer[3])
|
|
||||||
.putInt(trak.tkhd.bWidth)
|
|
||||||
.putInt(trak.tkhd.bHeight);
|
|
||||||
|
|
||||||
return lengthFor(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[][] make_moov() throws RuntimeException {
|
|
||||||
int pos = 1;
|
|
||||||
byte[][] buffer = new byte[2 + (DIMENSIONAL_TWO * infoTracks.length) + (DIMENSIONAL_FIVE * infoTracks.length) + DIMENSIONAL_FIVE + 1][];
|
|
||||||
|
|
||||||
buffer[0] = new byte[]{
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x6F, 0x76
|
|
||||||
};
|
|
||||||
|
|
||||||
for (byte[] buff : make_mvhd()) {
|
|
||||||
buffer[pos++] = buff;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < infoTracks.length; i++) {
|
|
||||||
for (byte[] buff : make_trak(i + 1, infoTracks[i].trak)) {
|
|
||||||
buffer[pos++] = buff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer[pos] = new byte[]{
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x6D, 0x76, 0x65, 0x78
|
|
||||||
};
|
|
||||||
|
|
||||||
ByteBuffer.wrap(buffer[pos++]).putInt((infoTracks.length * DEFAULT_TREX_SIZE) + 8);
|
|
||||||
|
|
||||||
for (int i = 0; i < infoTracks.length; i++) {
|
|
||||||
for (byte[] buff : make_trex(i + 1, infoTracks[i].trex)) {
|
|
||||||
buffer[pos++] = buff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// default udta
|
|
||||||
buffer[pos] = new byte[]{
|
|
||||||
0x00, 0x00, 0x00, 0x5C, 0x75, 0x64, 0x74, 0x61, 0x00, 0x00, 0x00, 0x54, 0x6D, 0x65, 0x74, 0x61,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x68, 0x64, 0x6C, 0x72, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x72, 0x61, 0x70, 0x70, 0x6C, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x69, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00,
|
|
||||||
0x1F, (byte) 0xA9, 0x63, 0x6D, 0x74, 0x00, 0x00, 0x00, 0x17, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00, 0x00,
|
|
||||||
0x01, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string
|
|
||||||
};
|
|
||||||
|
|
||||||
return lengthFor(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[][] make_trex(int trackId, Trex trex) {
|
|
||||||
byte[][] buffer = new byte[][]{
|
|
||||||
{
|
|
||||||
0x00, 0x00, 0x00, 0x20, 0x74, 0x72, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00
|
|
||||||
},
|
|
||||||
new byte[20]
|
|
||||||
};
|
|
||||||
|
|
||||||
ByteBuffer.wrap(buffer[1])
|
|
||||||
.putInt(trackId)
|
|
||||||
.putInt(trex.defaultSampleDescriptionIndex)
|
|
||||||
.putInt(trex.defaultSampleDuration)
|
|
||||||
.putInt(trex.defaultSampleSize)
|
|
||||||
.putInt(trex.defaultSampleFlags);
|
|
||||||
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[][] make_tfra(int trackId, List<Integer> times, List<Long> moofOffsets) {
|
|
||||||
int entryCount = times.size() - 1;
|
|
||||||
byte[][] buffer = new byte[DIMENSIONAL_TWO][];
|
|
||||||
buffer[0] = new byte[]{
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x74, 0x66, 0x72, 0x61, 0x01, 0x00, 0x00, 0x00
|
|
||||||
};
|
|
||||||
buffer[1] = new byte[12 + ((16 + TFRA_TTS_DEFAULT.length) * entryCount)];
|
|
||||||
|
|
||||||
ByteBuffer set = ByteBuffer.wrap(buffer[1]);
|
|
||||||
set.putInt(trackId);
|
|
||||||
set.position(8);
|
|
||||||
set.putInt(entryCount);
|
|
||||||
|
|
||||||
long decodeTime = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < entryCount; i++) {
|
|
||||||
decodeTime += times.get(i);
|
|
||||||
set.putLong(decodeTime);
|
|
||||||
set.putLong(moofOffsets.get(i));
|
|
||||||
set.put(TFRA_TTS_DEFAULT);// default values: traf number/trun number/sample number
|
|
||||||
}
|
|
||||||
|
|
||||||
return lengthFor(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[][] make_mfra() {
|
|
||||||
byte[][] buffer = new byte[2 + (DIMENSIONAL_TWO * infoTracks.length)][];
|
|
||||||
buffer[0] = new byte[]{
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x6D, 0x66, 0x72, 0x61
|
|
||||||
};
|
|
||||||
int pos = 1;
|
|
||||||
|
|
||||||
for (int i = 0; i < infoTracks.length; i++) {
|
|
||||||
for (byte[] buff : make_tfra(i + 1, chunkTimes.get(i), moofOffsets)) {
|
|
||||||
buffer[pos++] = buff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer[pos] = new byte[]{// mfro
|
|
||||||
0x00, 0x00, 0x00, 0x10, 0x6D, 0x66, 0x72, 0x6F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
|
||||||
};
|
|
||||||
|
|
||||||
lengthFor(buffer);
|
|
||||||
|
|
||||||
ByteBuffer set = ByteBuffer.wrap(buffer[pos]);
|
|
||||||
set.position(12);
|
|
||||||
set.put(buffer[0], 0, 4);
|
|
||||||
|
|
||||||
return buffer;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[][] make_sidx(int internalTrackId, long firstOffset) {
|
|
||||||
List<Integer> times = chunkTimes.get(internalTrackId);
|
|
||||||
int count = times.size() - 1;// the first item is ignored (composition time)
|
|
||||||
|
|
||||||
if (count > 65535) {
|
|
||||||
throw new OutOfMemoryError("to many fragments. sidx limit is 65535, found " + String.valueOf(count));
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[][] buffer = new byte[][]{
|
|
||||||
new byte[]{
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x73, 0x69, 0x64, 0x78, 0x01, 0x00, 0x00, 0x00
|
|
||||||
},
|
|
||||||
new byte[calcSidxBodySize(count)]
|
|
||||||
};
|
|
||||||
|
|
||||||
lengthFor(buffer);
|
|
||||||
|
|
||||||
ByteBuffer set = ByteBuffer.wrap(buffer[1]);
|
|
||||||
set.putInt(internalTrackId + 1);
|
|
||||||
set.putInt(infoTracks[internalTrackId].trak.mdia_mdhd_timeScale);
|
|
||||||
set.putLong(0);
|
|
||||||
set.putLong(firstOffset - ByteBuffer.wrap(buffer[0]).getInt());
|
|
||||||
set.putInt(0xFFFF & count);// unsigned
|
|
||||||
|
|
||||||
int i = 0;
|
|
||||||
while (i < count) {
|
|
||||||
set.putInt(fragSizes.get(i) & 0x7fffffff);// default reference type is 0
|
|
||||||
set.putInt(times.get(i + 1));
|
|
||||||
set.putInt(0x90000000);// default SAP settings
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[][] make_free(int totalSize) {
|
|
||||||
return lengthFor(new byte[][]{
|
|
||||||
new byte[]{0x00, 0x00, 0x00, 0x00, 0x66, 0x72, 0x65, 0x65},
|
|
||||||
new byte[totalSize - 8]// this is waste of RAM
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
//</editor-fold>
|
|
||||||
|
|
||||||
class TrunExtra {
|
|
||||||
|
|
||||||
ByteBuffer byteBuffer;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,810 @@
|
|||||||
|
package org.schabi.newpipe.streams;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.streams.Mp4DashReader.Hdlr;
|
||||||
|
import org.schabi.newpipe.streams.Mp4DashReader.Mdia;
|
||||||
|
import org.schabi.newpipe.streams.Mp4DashReader.Mp4DashChunk;
|
||||||
|
import org.schabi.newpipe.streams.Mp4DashReader.Mp4DashSample;
|
||||||
|
import org.schabi.newpipe.streams.Mp4DashReader.Mp4Track;
|
||||||
|
import org.schabi.newpipe.streams.Mp4DashReader.TrunEntry;
|
||||||
|
import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author kapodamy
|
||||||
|
*/
|
||||||
|
public class Mp4FromDashWriter {
|
||||||
|
|
||||||
|
private final static int EPOCH_OFFSET = 2082844800;
|
||||||
|
private final static short DEFAULT_TIMESCALE = 1000;
|
||||||
|
private final static byte SAMPLES_PER_CHUNK_INIT = 2;
|
||||||
|
private final static byte SAMPLES_PER_CHUNK = 6;// ffmpeg uses 2, basic uses 1 (with 60fps uses 21 or 22). NewPipe will use 6
|
||||||
|
private final static long THRESHOLD_FOR_CO64 = 0xFFFEFFFFL;// near 3.999 GiB
|
||||||
|
private final static int THRESHOLD_MOOV_LENGTH = (256 * 1024) + (2048 * 1024); // 2.2 MiB enough for: 1080p 60fps 00h35m00s
|
||||||
|
|
||||||
|
private final long time;
|
||||||
|
|
||||||
|
private ByteBuffer auxBuffer;
|
||||||
|
private SharpStream outStream;
|
||||||
|
|
||||||
|
private long lastWriteOffset = -1;
|
||||||
|
private long writeOffset;
|
||||||
|
|
||||||
|
private boolean moovSimulation = true;
|
||||||
|
|
||||||
|
private boolean done = false;
|
||||||
|
private boolean parsed = false;
|
||||||
|
|
||||||
|
private Mp4Track[] tracks;
|
||||||
|
private SharpStream[] sourceTracks;
|
||||||
|
|
||||||
|
private Mp4DashReader[] readers;
|
||||||
|
private Mp4DashChunk[] readersChunks;
|
||||||
|
|
||||||
|
private int overrideMainBrand = 0x00;
|
||||||
|
|
||||||
|
public Mp4FromDashWriter(SharpStream... sources) throws IOException {
|
||||||
|
for (SharpStream src : sources) {
|
||||||
|
if (!src.canRewind() && !src.canRead()) {
|
||||||
|
throw new IOException("All sources must be readable and allow rewind");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceTracks = sources;
|
||||||
|
readers = new Mp4DashReader[sourceTracks.length];
|
||||||
|
readersChunks = new Mp4DashChunk[readers.length];
|
||||||
|
time = (System.currentTimeMillis() / 1000L) + EPOCH_OFFSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mp4Track[] getTracksFromSource(int sourceIndex) throws IllegalStateException {
|
||||||
|
if (!parsed) {
|
||||||
|
throw new IllegalStateException("All sources must be parsed first");
|
||||||
|
}
|
||||||
|
|
||||||
|
return readers[sourceIndex].getAvailableTracks();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void parseSources() throws IOException, IllegalStateException {
|
||||||
|
if (done) {
|
||||||
|
throw new IllegalStateException("already done");
|
||||||
|
}
|
||||||
|
if (parsed) {
|
||||||
|
throw new IllegalStateException("already parsed");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < readers.length; i++) {
|
||||||
|
readers[i] = new Mp4DashReader(sourceTracks[i]);
|
||||||
|
readers[i].parse();
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
parsed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void selectTracks(int... trackIndex) throws IOException {
|
||||||
|
if (done) {
|
||||||
|
throw new IOException("already done");
|
||||||
|
}
|
||||||
|
if (tracks != null) {
|
||||||
|
throw new IOException("tracks already selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
tracks = new Mp4Track[readers.length];
|
||||||
|
for (int i = 0; i < readers.length; i++) {
|
||||||
|
tracks[i] = readers[i].selectTrack(trackIndex[i]);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
parsed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMainBrand(int brandId) {
|
||||||
|
overrideMainBrand = brandId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDone() {
|
||||||
|
return done;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isParsed() {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() throws IOException {
|
||||||
|
done = true;
|
||||||
|
parsed = true;
|
||||||
|
|
||||||
|
for (SharpStream src : sourceTracks) {
|
||||||
|
src.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
tracks = null;
|
||||||
|
sourceTracks = null;
|
||||||
|
|
||||||
|
readers = null;
|
||||||
|
readersChunks = null;
|
||||||
|
|
||||||
|
auxBuffer = null;
|
||||||
|
outStream = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void build(SharpStream output) throws IOException {
|
||||||
|
if (done) {
|
||||||
|
throw new RuntimeException("already done");
|
||||||
|
}
|
||||||
|
if (!output.canWrite()) {
|
||||||
|
throw new IOException("the provided output is not writable");
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// WARNING: the muxer requires at least 8 samples of every track
|
||||||
|
// not allowed for very short tracks (less than 0.5 seconds)
|
||||||
|
//
|
||||||
|
outStream = output;
|
||||||
|
int read = 8;// mdat box header size
|
||||||
|
long totalSampleSize = 0;
|
||||||
|
int[] sampleExtra = new int[readers.length];
|
||||||
|
int[] defaultMediaTime = new int[readers.length];
|
||||||
|
int[] defaultSampleDuration = new int[readers.length];
|
||||||
|
int[] sampleCount = new int[readers.length];
|
||||||
|
|
||||||
|
TablesInfo[] tablesInfo = new TablesInfo[tracks.length];
|
||||||
|
for (int i = 0; i < tablesInfo.length; i++) {
|
||||||
|
tablesInfo[i] = new TablesInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
//<editor-fold defaultstate="expanded" desc="calculate stbl sample tables size and required moov values">
|
||||||
|
for (int i = 0; i < readers.length; i++) {
|
||||||
|
int samplesSize = 0;
|
||||||
|
int sampleSizeChanges = 0;
|
||||||
|
int compositionOffsetLast = -1;
|
||||||
|
|
||||||
|
Mp4DashChunk chunk;
|
||||||
|
while ((chunk = readers[i].getNextChunk(true)) != null) {
|
||||||
|
|
||||||
|
if (defaultMediaTime[i] < 1 && chunk.moof.traf.tfhd.defaultSampleDuration > 0) {
|
||||||
|
defaultMediaTime[i] = chunk.moof.traf.tfhd.defaultSampleDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
read += chunk.moof.traf.trun.chunkSize;
|
||||||
|
sampleExtra[i] += chunk.moof.traf.trun.chunkDuration;// calculate track duration
|
||||||
|
|
||||||
|
TrunEntry info;
|
||||||
|
while ((info = chunk.getNextSampleInfo()) != null) {
|
||||||
|
if (info.isKeyframe) {
|
||||||
|
tablesInfo[i].stss++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.sampleDuration > defaultSampleDuration[i]) {
|
||||||
|
defaultSampleDuration[i] = info.sampleDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
tablesInfo[i].stsz++;
|
||||||
|
if (samplesSize != info.sampleSize) {
|
||||||
|
samplesSize = info.sampleSize;
|
||||||
|
sampleSizeChanges++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.hasCompositionTimeOffset) {
|
||||||
|
if (info.sampleCompositionTimeOffset != compositionOffsetLast) {
|
||||||
|
tablesInfo[i].ctts++;
|
||||||
|
compositionOffsetLast = info.sampleCompositionTimeOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
totalSampleSize += info.sampleSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultMediaTime[i] < 1) {
|
||||||
|
defaultMediaTime[i] = defaultSampleDuration[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
readers[i].rewind();
|
||||||
|
|
||||||
|
int tmp = tablesInfo[i].stsz - SAMPLES_PER_CHUNK_INIT;
|
||||||
|
tablesInfo[i].stco = (tmp / SAMPLES_PER_CHUNK) + 1;// +1 for samples in first chunk
|
||||||
|
|
||||||
|
tmp = tmp % SAMPLES_PER_CHUNK;
|
||||||
|
if (tmp == 0) {
|
||||||
|
tablesInfo[i].stsc = 2;// first chunk (init) and succesive chunks
|
||||||
|
tablesInfo[i].stsc_bEntries = new int[]{
|
||||||
|
1, SAMPLES_PER_CHUNK_INIT, 1,
|
||||||
|
2, SAMPLES_PER_CHUNK, 1
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
tablesInfo[i].stsc = 3;// first chunk (init) and succesive chunks and remain chunk
|
||||||
|
tablesInfo[i].stsc_bEntries = new int[]{
|
||||||
|
1, SAMPLES_PER_CHUNK_INIT, 1,
|
||||||
|
2, SAMPLES_PER_CHUNK, 1,
|
||||||
|
tablesInfo[i].stco + 1, tmp, 1
|
||||||
|
};
|
||||||
|
tablesInfo[i].stco++;
|
||||||
|
}
|
||||||
|
|
||||||
|
sampleCount[i] = tablesInfo[i].stsz;
|
||||||
|
|
||||||
|
if (sampleSizeChanges == 1) {
|
||||||
|
tablesInfo[i].stsz = 0;
|
||||||
|
tablesInfo[i].stsz_default = samplesSize;
|
||||||
|
} else {
|
||||||
|
tablesInfo[i].stsz_default = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tablesInfo[i].stss == tablesInfo[i].stsz) {
|
||||||
|
tablesInfo[i].stss = -1;// for audio tracks (all samples are keyframes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure track duration
|
||||||
|
if (tracks[i].trak.tkhd.duration < 1) {
|
||||||
|
tracks[i].trak.tkhd.duration = sampleExtra[i];// this never should happen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//</editor-fold>
|
||||||
|
|
||||||
|
boolean is64 = read > THRESHOLD_FOR_CO64;
|
||||||
|
|
||||||
|
// calculate the moov size;
|
||||||
|
int auxSize = make_moov(defaultMediaTime, tablesInfo, is64);
|
||||||
|
|
||||||
|
if (auxSize < THRESHOLD_MOOV_LENGTH) {
|
||||||
|
auxBuffer = ByteBuffer.allocate(auxSize);// cache moov in the memory
|
||||||
|
}
|
||||||
|
|
||||||
|
moovSimulation = false;
|
||||||
|
writeOffset = 0;
|
||||||
|
|
||||||
|
final int ftyp_size = make_ftyp();
|
||||||
|
|
||||||
|
// reserve moov space in the output stream
|
||||||
|
/*if (outStream.canSetLength()) {
|
||||||
|
long length = writeOffset + auxSize;
|
||||||
|
outStream.setLength(length);
|
||||||
|
outSeek(length);
|
||||||
|
} else {*/
|
||||||
|
if (auxSize > 0) {
|
||||||
|
int length = auxSize;
|
||||||
|
byte[] buffer = new byte[8 * 1024];// 8 KiB
|
||||||
|
while (length > 0) {
|
||||||
|
int count = Math.min(length, buffer.length);
|
||||||
|
outWrite(buffer, 0, count);
|
||||||
|
length -= count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auxBuffer == null) {
|
||||||
|
outSeek(ftyp_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// tablesInfo contais row counts
|
||||||
|
// and after returning from make_moov() will contain table offsets
|
||||||
|
make_moov(defaultMediaTime, tablesInfo, is64);
|
||||||
|
|
||||||
|
// write tables: stts stsc
|
||||||
|
// reset for ctts table: sampleCount sampleExtra
|
||||||
|
for (int i = 0; i < readers.length; i++) {
|
||||||
|
writeEntryArray(tablesInfo[i].stts, 2, sampleCount[i], defaultSampleDuration[i]);
|
||||||
|
writeEntryArray(tablesInfo[i].stsc, tablesInfo[i].stsc_bEntries.length, tablesInfo[i].stsc_bEntries);
|
||||||
|
tablesInfo[i].stsc_bEntries = null;
|
||||||
|
if (tablesInfo[i].ctts > 0) {
|
||||||
|
sampleCount[i] = 1;// index is not base zero
|
||||||
|
sampleExtra[i] = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auxBuffer == null) {
|
||||||
|
outRestore();
|
||||||
|
}
|
||||||
|
|
||||||
|
outWrite(make_mdat(totalSampleSize, is64));
|
||||||
|
|
||||||
|
int[] sampleIndex = new int[readers.length];
|
||||||
|
int[] sizes = new int[SAMPLES_PER_CHUNK];
|
||||||
|
int[] sync = new int[SAMPLES_PER_CHUNK];
|
||||||
|
|
||||||
|
int written = readers.length;
|
||||||
|
while (written > 0) {
|
||||||
|
written = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < readers.length; i++) {
|
||||||
|
if (sampleIndex[i] < 0) {
|
||||||
|
continue;// track is done
|
||||||
|
}
|
||||||
|
|
||||||
|
long chunkOffset = writeOffset;
|
||||||
|
int syncCount = 0;
|
||||||
|
int limit = sampleIndex[i] == 0 ? SAMPLES_PER_CHUNK_INIT : SAMPLES_PER_CHUNK;
|
||||||
|
|
||||||
|
int j = 0;
|
||||||
|
for (; j < limit; j++) {
|
||||||
|
Mp4DashSample sample = getNextSample(i);
|
||||||
|
|
||||||
|
if (sample == null) {
|
||||||
|
if (tablesInfo[i].ctts > 0 && sampleExtra[i] >= 0) {
|
||||||
|
writeEntryArray(tablesInfo[i].ctts, 1, sampleCount[i], sampleExtra[i]);// flush last entries
|
||||||
|
}
|
||||||
|
sampleIndex[i] = -1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
sampleIndex[i]++;
|
||||||
|
|
||||||
|
if (tablesInfo[i].ctts > 0) {
|
||||||
|
if (sample.info.sampleCompositionTimeOffset == sampleExtra[i]) {
|
||||||
|
sampleCount[i]++;
|
||||||
|
} else {
|
||||||
|
if (sampleExtra[i] >= 0) {
|
||||||
|
tablesInfo[i].ctts = writeEntryArray(tablesInfo[i].ctts, 2, sampleCount[i], sampleExtra[i]);
|
||||||
|
outRestore();
|
||||||
|
}
|
||||||
|
sampleCount[i] = 1;
|
||||||
|
sampleExtra[i] = sample.info.sampleCompositionTimeOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tablesInfo[i].stss > 0 && sample.info.isKeyframe) {
|
||||||
|
sync[syncCount++] = sampleIndex[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tablesInfo[i].stsz > 0) {
|
||||||
|
sizes[j] = sample.data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
outWrite(sample.data, 0, sample.data.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (j > 0) {
|
||||||
|
written++;
|
||||||
|
|
||||||
|
if (tablesInfo[i].stsz > 0) {
|
||||||
|
tablesInfo[i].stsz = writeEntryArray(tablesInfo[i].stsz, j, sizes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (syncCount > 0) {
|
||||||
|
tablesInfo[i].stss = writeEntryArray(tablesInfo[i].stss, syncCount, sync);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is64) {
|
||||||
|
tablesInfo[i].stco = writeEntry64(tablesInfo[i].stco, chunkOffset);
|
||||||
|
} else {
|
||||||
|
tablesInfo[i].stco = writeEntryArray(tablesInfo[i].stco, 1, (int) chunkOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
outRestore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auxBuffer != null) {
|
||||||
|
// dump moov
|
||||||
|
outSeek(ftyp_size);
|
||||||
|
outStream.write(auxBuffer.array(), 0, auxBuffer.capacity());
|
||||||
|
auxBuffer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mp4DashSample getNextSample(int track) throws IOException {
|
||||||
|
if (readersChunks[track] == null) {
|
||||||
|
readersChunks[track] = readers[track].getNextChunk(false);
|
||||||
|
if (readersChunks[track] == null) {
|
||||||
|
return null;// EOF reached
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Mp4DashSample sample = readersChunks[track].getNextSample();
|
||||||
|
if (sample == null) {
|
||||||
|
readersChunks[track] = null;
|
||||||
|
return getNextSample(track);
|
||||||
|
} else {
|
||||||
|
return sample;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// <editor-fold defaultstate="expanded" desc="Stbl handling">
|
||||||
|
private int writeEntry64(int offset, long value) throws IOException {
|
||||||
|
outBackup();
|
||||||
|
|
||||||
|
auxSeek(offset);
|
||||||
|
auxWrite(ByteBuffer.allocate(8).putLong(value).array());
|
||||||
|
|
||||||
|
return offset + 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int writeEntryArray(int offset, int count, int... values) throws IOException {
|
||||||
|
outBackup();
|
||||||
|
|
||||||
|
auxSeek(offset);
|
||||||
|
|
||||||
|
int size = count * 4;
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(size);
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
buffer.putInt(values[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
auxWrite(buffer.array());
|
||||||
|
|
||||||
|
return offset + size;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void outBackup() {
|
||||||
|
if (auxBuffer == null && lastWriteOffset < 0) {
|
||||||
|
lastWriteOffset = writeOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore to the previous position before the first call to writeEntry64()
|
||||||
|
* or writeEntryArray() methods.
|
||||||
|
*/
|
||||||
|
private void outRestore() throws IOException {
|
||||||
|
if (lastWriteOffset > 0) {
|
||||||
|
outSeek(lastWriteOffset);
|
||||||
|
lastWriteOffset = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// </editor-fold>
|
||||||
|
|
||||||
|
// <editor-fold defaultstate="expanded" desc="Utils">
|
||||||
|
private void outWrite(byte[] buffer) throws IOException {
|
||||||
|
outWrite(buffer, 0, buffer.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void outWrite(byte[] buffer, int offset, int count) throws IOException {
|
||||||
|
writeOffset += count;
|
||||||
|
outStream.write(buffer, offset, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void outSeek(long offset) throws IOException {
|
||||||
|
if (outStream.canSeek()) {
|
||||||
|
outStream.seek(offset);
|
||||||
|
writeOffset = offset;
|
||||||
|
} else if (outStream.canRewind()) {
|
||||||
|
outStream.rewind();
|
||||||
|
writeOffset = 0;
|
||||||
|
outSkip(offset);
|
||||||
|
} else {
|
||||||
|
throw new IOException("cannot seek or rewind the output stream");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void outSkip(long amount) throws IOException {
|
||||||
|
outStream.skip(amount);
|
||||||
|
writeOffset += amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int lengthFor(int offset) throws IOException {
|
||||||
|
int size = auxOffset() - offset;
|
||||||
|
|
||||||
|
if (moovSimulation) {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
auxSeek(offset);
|
||||||
|
auxWrite(size);
|
||||||
|
auxSkip(size - 4);
|
||||||
|
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int make(int type, int extra, int columns, int rows) throws IOException {
|
||||||
|
final byte base = 16;
|
||||||
|
int size = columns * rows * 4;
|
||||||
|
int total = size + base;
|
||||||
|
int offset = auxOffset();
|
||||||
|
|
||||||
|
if (extra >= 0) {
|
||||||
|
total += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
auxWrite(ByteBuffer.allocate(12)
|
||||||
|
.putInt(total)
|
||||||
|
.putInt(type)
|
||||||
|
.putInt(0x00)// default version & flags
|
||||||
|
.array()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (extra >= 0) {
|
||||||
|
//size += 4;// commented for auxiliar buffer !!!
|
||||||
|
offset += 4;
|
||||||
|
auxWrite(extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
auxWrite(rows);
|
||||||
|
auxSkip(size);
|
||||||
|
|
||||||
|
return offset + base;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void auxWrite(int value) throws IOException {
|
||||||
|
auxWrite(ByteBuffer.allocate(4)
|
||||||
|
.putInt(value)
|
||||||
|
.array()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void auxWrite(byte[] buffer) throws IOException {
|
||||||
|
if (moovSimulation) {
|
||||||
|
writeOffset += buffer.length;
|
||||||
|
} else if (auxBuffer == null) {
|
||||||
|
outWrite(buffer, 0, buffer.length);
|
||||||
|
} else {
|
||||||
|
auxBuffer.put(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void auxSeek(int offset) throws IOException {
|
||||||
|
if (moovSimulation) {
|
||||||
|
writeOffset = offset;
|
||||||
|
} else if (auxBuffer == null) {
|
||||||
|
outSeek(offset);
|
||||||
|
} else {
|
||||||
|
auxBuffer.position(offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void auxSkip(int amount) throws IOException {
|
||||||
|
if (moovSimulation) {
|
||||||
|
writeOffset += amount;
|
||||||
|
} else if (auxBuffer == null) {
|
||||||
|
outSkip(amount);
|
||||||
|
} else {
|
||||||
|
auxBuffer.position(auxBuffer.position() + amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int auxOffset() {
|
||||||
|
return auxBuffer == null ? (int) writeOffset : auxBuffer.position();
|
||||||
|
}
|
||||||
|
// </editor-fold>
|
||||||
|
|
||||||
|
// <editor-fold defaultstate="expanded" desc="Box makers">
|
||||||
|
private int make_ftyp() throws IOException {
|
||||||
|
byte[] buffer = new byte[]{
|
||||||
|
0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70,// ftyp
|
||||||
|
0x6D, 0x70, 0x34, 0x32,// mayor brand (mp42)
|
||||||
|
0x00, 0x00, 0x02, 0x00,// default minor version (512)
|
||||||
|
0x6D, 0x70, 0x34, 0x31, 0x69, 0x73, 0x6F, 0x6D, 0x69, 0x73, 0x6F, 0x32// compatible brands: mp41 isom iso2
|
||||||
|
};
|
||||||
|
|
||||||
|
if (overrideMainBrand != 0)
|
||||||
|
ByteBuffer.wrap(buffer).putInt(8, overrideMainBrand);
|
||||||
|
|
||||||
|
outWrite(buffer);
|
||||||
|
|
||||||
|
return buffer.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] make_mdat(long refSize, boolean is64) {
|
||||||
|
if (is64) {
|
||||||
|
refSize += 16;
|
||||||
|
} else {
|
||||||
|
refSize += 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(is64 ? 16 : 8)
|
||||||
|
.putInt(is64 ? 0x01 : (int) refSize)
|
||||||
|
.putInt(0x6D646174);// mdat
|
||||||
|
|
||||||
|
if (is64) {
|
||||||
|
buffer.putLong(refSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void make_mvhd(long longestTrack) throws IOException {
|
||||||
|
auxWrite(new byte[]{
|
||||||
|
0x00, 0x00, 0x00, 0x78, 0x6D, 0x76, 0x68, 0x64, 0x01, 0x00, 0x00, 0x00
|
||||||
|
});
|
||||||
|
auxWrite(ByteBuffer.allocate(28)
|
||||||
|
.putLong(time)
|
||||||
|
.putLong(time)
|
||||||
|
.putInt(DEFAULT_TIMESCALE)
|
||||||
|
.putLong(longestTrack)
|
||||||
|
.array()
|
||||||
|
);
|
||||||
|
|
||||||
|
auxWrite(new byte[]{
|
||||||
|
0x00, 0x01, 0x00, 0x00, 0x01, 0x00,// default volume and rate
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,// reserved values
|
||||||
|
// default matrix
|
||||||
|
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x40, 0x00, 0x00, 0x00
|
||||||
|
});
|
||||||
|
auxWrite(new byte[24]);// predefined
|
||||||
|
auxWrite(ByteBuffer.allocate(4)
|
||||||
|
.putInt(tracks.length + 1)
|
||||||
|
.array()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int make_moov(int[] defaultMediaTime, TablesInfo[] tablesInfo, boolean is64) throws RuntimeException, IOException {
|
||||||
|
int start = auxOffset();
|
||||||
|
|
||||||
|
auxWrite(new byte[]{
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x6F, 0x76
|
||||||
|
});
|
||||||
|
|
||||||
|
long longestTrack = 0;
|
||||||
|
long[] durations = new long[tracks.length];
|
||||||
|
|
||||||
|
for (int i = 0; i < durations.length; i++) {
|
||||||
|
durations[i] = (long) Math.ceil(
|
||||||
|
((double) tracks[i].trak.tkhd.duration / tracks[i].trak.mdia.mdhd_timeScale) * DEFAULT_TIMESCALE
|
||||||
|
);
|
||||||
|
|
||||||
|
if (durations[i] > longestTrack) {
|
||||||
|
longestTrack = durations[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
make_mvhd(longestTrack);
|
||||||
|
|
||||||
|
for (int i = 0; i < tracks.length; i++) {
|
||||||
|
if (tracks[i].trak.tkhd.matrix.length != 36) {
|
||||||
|
throw new RuntimeException("bad track matrix length (expected 36) in track n°" + i);
|
||||||
|
}
|
||||||
|
make_trak(i, durations[i], defaultMediaTime[i], tablesInfo[i], is64);
|
||||||
|
}
|
||||||
|
|
||||||
|
// udta/meta/ilst/©too
|
||||||
|
auxWrite(new byte[]{
|
||||||
|
0x00, 0x00, 0x00, 0x5C, 0x75, 0x64, 0x74, 0x61, 0x00, 0x00, 0x00, 0x54, 0x6D, 0x65, 0x74, 0x61,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x68, 0x64, 0x6C, 0x72, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x72, 0x61, 0x70, 0x70, 0x6C, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x69, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00,
|
||||||
|
0x1F, (byte) 0xA9, 0x74, 0x6F, 0x6F, 0x00, 0x00, 0x00, 0x17, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00,
|
||||||
|
0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string
|
||||||
|
});
|
||||||
|
|
||||||
|
return lengthFor(start);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void make_trak(int index, long duration, int defaultMediaTime, TablesInfo tables, boolean is64) throws IOException {
|
||||||
|
int start = auxOffset();
|
||||||
|
|
||||||
|
auxWrite(new byte[]{
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x6B,// trak header
|
||||||
|
0x00, 0x00, 0x00, 0x68, 0x74, 0x6B, 0x68, 0x64, 0x01, 0x00, 0x00, 0x03 // tkhd header
|
||||||
|
});
|
||||||
|
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(48);
|
||||||
|
buffer.putLong(time);
|
||||||
|
buffer.putLong(time);
|
||||||
|
buffer.putInt(index + 1);
|
||||||
|
buffer.position(24);
|
||||||
|
buffer.putLong(duration);
|
||||||
|
buffer.position(40);
|
||||||
|
buffer.putShort(tracks[index].trak.tkhd.bLayer);
|
||||||
|
buffer.putShort(tracks[index].trak.tkhd.bAlternateGroup);
|
||||||
|
buffer.putShort(tracks[index].trak.tkhd.bVolume);
|
||||||
|
auxWrite(buffer.array());
|
||||||
|
|
||||||
|
auxWrite(tracks[index].trak.tkhd.matrix);
|
||||||
|
auxWrite(ByteBuffer.allocate(8)
|
||||||
|
.putInt(tracks[index].trak.tkhd.bWidth)
|
||||||
|
.putInt(tracks[index].trak.tkhd.bHeight)
|
||||||
|
.array()
|
||||||
|
);
|
||||||
|
|
||||||
|
auxWrite(new byte[]{
|
||||||
|
0x00, 0x00, 0x00, 0x24, 0x65, 0x64, 0x74, 0x73,// edts header
|
||||||
|
0x00, 0x00, 0x00, 0x1C, 0x65, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01// elst header
|
||||||
|
});
|
||||||
|
|
||||||
|
int bMediaRate;
|
||||||
|
int mediaTime;
|
||||||
|
|
||||||
|
if (tracks[index].trak.edst_elst == null) {
|
||||||
|
// is a audio track ¿is edst/elst opcional for audio tracks?
|
||||||
|
mediaTime = 0x00;// ffmpeg set this value as zero, instead of defaultMediaTime
|
||||||
|
bMediaRate = 0x00010000;
|
||||||
|
} else {
|
||||||
|
mediaTime = (int) tracks[index].trak.edst_elst.MediaTime;
|
||||||
|
bMediaRate = tracks[index].trak.edst_elst.bMediaRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
auxWrite(ByteBuffer
|
||||||
|
.allocate(12)
|
||||||
|
.putInt((int) duration)
|
||||||
|
.putInt(mediaTime)
|
||||||
|
.putInt(bMediaRate)
|
||||||
|
.array()
|
||||||
|
);
|
||||||
|
|
||||||
|
make_mdia(tracks[index].trak.mdia, tables, is64);
|
||||||
|
|
||||||
|
lengthFor(start);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void make_mdia(Mdia mdia, TablesInfo tablesInfo, boolean is64) throws IOException {
|
||||||
|
|
||||||
|
int start_mdia = auxOffset();
|
||||||
|
auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x61});// mdia
|
||||||
|
auxWrite(mdia.mdhd);
|
||||||
|
auxWrite(make_hdlr(mdia.hdlr));
|
||||||
|
|
||||||
|
int start_minf = auxOffset();
|
||||||
|
auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x69, 0x6E, 0x66});// minf
|
||||||
|
auxWrite(mdia.minf.$mhd);
|
||||||
|
auxWrite(mdia.minf.dinf);
|
||||||
|
|
||||||
|
int start_stbl = auxOffset();
|
||||||
|
auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x73, 0x74, 0x62, 0x6C});// stbl
|
||||||
|
auxWrite(mdia.minf.stbl_stsd);
|
||||||
|
|
||||||
|
//
|
||||||
|
// In audio tracks the following tables is not required: ssts ctts
|
||||||
|
// And stsz can be empty if has a default sample size
|
||||||
|
//
|
||||||
|
if (moovSimulation) {
|
||||||
|
make(0x73747473, -1, 2, 1);
|
||||||
|
if (tablesInfo.stss > 0) {
|
||||||
|
make(0x73747373, -1, 1, tablesInfo.stss);
|
||||||
|
}
|
||||||
|
if (tablesInfo.ctts > 0) {
|
||||||
|
make(0x63747473, -1, 2, tablesInfo.ctts);
|
||||||
|
}
|
||||||
|
make(0x73747363, -1, 3, tablesInfo.stsc);
|
||||||
|
make(0x7374737A, tablesInfo.stsz_default, 1, tablesInfo.stsz);
|
||||||
|
make(is64 ? 0x636F3634 : 0x7374636F, -1, is64 ? 2 : 1, tablesInfo.stco);
|
||||||
|
} else {
|
||||||
|
tablesInfo.stts = make(0x73747473, -1, 2, 1);
|
||||||
|
if (tablesInfo.stss > 0) {
|
||||||
|
tablesInfo.stss = make(0x73747373, -1, 1, tablesInfo.stss);
|
||||||
|
}
|
||||||
|
if (tablesInfo.ctts > 0) {
|
||||||
|
tablesInfo.ctts = make(0x63747473, -1, 2, tablesInfo.ctts);
|
||||||
|
}
|
||||||
|
tablesInfo.stsc = make(0x73747363, -1, 3, tablesInfo.stsc);
|
||||||
|
tablesInfo.stsz = make(0x7374737A, tablesInfo.stsz_default, 1, tablesInfo.stsz);
|
||||||
|
tablesInfo.stco = make(is64 ? 0x636F3634 : 0x7374636F, -1, is64 ? 2 : 1, tablesInfo.stco);
|
||||||
|
}
|
||||||
|
|
||||||
|
lengthFor(start_stbl);
|
||||||
|
lengthFor(start_minf);
|
||||||
|
lengthFor(start_mdia);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] make_hdlr(Hdlr hdlr) {
|
||||||
|
ByteBuffer buffer = ByteBuffer.wrap(new byte[]{
|
||||||
|
0x00, 0x00, 0x00, 0x77, 0x68, 0x64, 0x6C, 0x72,// hdlr
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// binary string "ISO Media file created in NewPipe (A libre lightweight streaming frontend for Android)."
|
||||||
|
0x49, 0x53, 0x4F, 0x20, 0x4D, 0x65, 0x64, 0x69, 0x61, 0x20, 0x66, 0x69, 0x6C, 0x65, 0x20, 0x63,
|
||||||
|
0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x20, 0x69, 0x6E, 0x20, 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70,
|
||||||
|
0x65, 0x20, 0x28, 0x41, 0x20, 0x6C, 0x69, 0x62, 0x72, 0x65, 0x20, 0x6C, 0x69, 0x67, 0x68, 0x74,
|
||||||
|
0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x20, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D, 0x69, 0x6E, 0x67,
|
||||||
|
0x20, 0x66, 0x72, 0x6F, 0x6E, 0x74, 0x65, 0x6E, 0x64, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x41, 0x6E,
|
||||||
|
0x64, 0x72, 0x6F, 0x69, 0x64, 0x29, 0x2E
|
||||||
|
});
|
||||||
|
|
||||||
|
buffer.position(12);
|
||||||
|
buffer.putInt(hdlr.type);
|
||||||
|
buffer.putInt(hdlr.subType);
|
||||||
|
buffer.put(hdlr.bReserved);// always is a zero array
|
||||||
|
|
||||||
|
return buffer.array();
|
||||||
|
}
|
||||||
|
//</editor-fold>
|
||||||
|
|
||||||
|
class TablesInfo {
|
||||||
|
|
||||||
|
public int stts;
|
||||||
|
public int stsc;
|
||||||
|
public int[] stsc_bEntries;
|
||||||
|
public int ctts;
|
||||||
|
public int stsz;
|
||||||
|
public int stsz_default;
|
||||||
|
public int stss;
|
||||||
|
public int stco;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package org.schabi.newpipe.streams;
|
package org.schabi.newpipe.streams;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
import org.w3c.dom.Element;
|
import org.w3c.dom.Element;
|
||||||
import org.w3c.dom.Node;
|
import org.w3c.dom.Node;
|
||||||
@ -12,8 +13,6 @@ import java.nio.charset.Charset;
|
|||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
import org.schabi.newpipe.streams.io.SharpStream;
|
|
||||||
|
|
||||||
import javax.xml.parsers.DocumentBuilder;
|
import javax.xml.parsers.DocumentBuilder;
|
||||||
import javax.xml.parsers.DocumentBuilderFactory;
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
import javax.xml.parsers.ParserConfigurationException;
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
@ -27,11 +26,11 @@ public class SubtitleConverter {
|
|||||||
|
|
||||||
public void dumpTTML(SharpStream in, final SharpStream out, final boolean ignoreEmptyFrames, final boolean detectYoutubeDuplicateLines
|
public void dumpTTML(SharpStream in, final SharpStream out, final boolean ignoreEmptyFrames, final boolean detectYoutubeDuplicateLines
|
||||||
) throws IOException, ParseException, SAXException, ParserConfigurationException, XPathExpressionException {
|
) throws IOException, ParseException, SAXException, ParserConfigurationException, XPathExpressionException {
|
||||||
|
|
||||||
final FrameWriter callback = new FrameWriter() {
|
final FrameWriter callback = new FrameWriter() {
|
||||||
int frameIndex = 0;
|
int frameIndex = 0;
|
||||||
final Charset charset = Charset.forName("utf-8");
|
final Charset charset = Charset.forName("utf-8");
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void yield(SubtitleFrame frame) throws IOException {
|
public void yield(SubtitleFrame frame) throws IOException {
|
||||||
if (ignoreEmptyFrames && frame.isEmptyText()) {
|
if (ignoreEmptyFrames && frame.isEmptyText()) {
|
||||||
@ -48,13 +47,13 @@ public class SubtitleConverter {
|
|||||||
out.write(NEW_LINE.getBytes(charset));
|
out.write(NEW_LINE.getBytes(charset));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
read_xml_based(in, callback, detectYoutubeDuplicateLines,
|
read_xml_based(in, callback, detectYoutubeDuplicateLines,
|
||||||
"tt", "xmlns", "http://www.w3.org/ns/ttml",
|
"tt", "xmlns", "http://www.w3.org/ns/ttml",
|
||||||
new String[]{"timedtext", "head", "wp"},
|
new String[]{"timedtext", "head", "wp"},
|
||||||
new String[]{"body", "div", "p"},
|
new String[]{"body", "div", "p"},
|
||||||
"begin", "end", true
|
"begin", "end", true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void read_xml_based(SharpStream source, FrameWriter callback, boolean detectYoutubeDuplicateLines,
|
private void read_xml_based(SharpStream source, FrameWriter callback, boolean detectYoutubeDuplicateLines,
|
||||||
@ -70,7 +69,7 @@ public class SubtitleConverter {
|
|||||||
* Language parsing is not supported
|
* Language parsing is not supported
|
||||||
*/
|
*/
|
||||||
|
|
||||||
byte[] buffer = new byte[source.available()];
|
byte[] buffer = new byte[(int) source.available()];
|
||||||
source.read(buffer);
|
source.read(buffer);
|
||||||
|
|
||||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||||
@ -206,7 +205,7 @@ public class SubtitleConverter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static NodeList selectNodes(Document xml, String[] path, String namespaceUri) throws XPathExpressionException {
|
private static NodeList selectNodes(Document xml, String[] path, String namespaceUri) {
|
||||||
Element ref = xml.getDocumentElement();
|
Element ref = xml.getDocumentElement();
|
||||||
|
|
||||||
for (int i = 0; i < path.length - 1; i++) {
|
for (int i = 0; i < path.length - 1; i++) {
|
||||||
|
@ -1,65 +0,0 @@
|
|||||||
package org.schabi.newpipe.streams;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
public class TrackDataChunk extends InputStream {
|
|
||||||
|
|
||||||
private final DataReader base;
|
|
||||||
private int size;
|
|
||||||
|
|
||||||
public TrackDataChunk(DataReader base, int size) {
|
|
||||||
this.base = base;
|
|
||||||
this.size = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read() throws IOException {
|
|
||||||
if (size < 1) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int res = base.read();
|
|
||||||
|
|
||||||
if (res >= 0) {
|
|
||||||
size--;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read(byte[] buffer) throws IOException {
|
|
||||||
return read(buffer, 0, buffer.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read(byte[] buffer, int offset, int count) throws IOException {
|
|
||||||
count = Math.min(size, count);
|
|
||||||
int read = base.read(buffer, offset, count);
|
|
||||||
size -= count;
|
|
||||||
return read;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long skip(long amount) throws IOException {
|
|
||||||
long res = base.skipBytes(Math.min(amount, size));
|
|
||||||
size -= res;
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int available() {
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
size = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean markSupported() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +1,13 @@
|
|||||||
package org.schabi.newpipe.streams;
|
package org.schabi.newpipe.streams;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
|
|
||||||
import java.io.EOFException;
|
import java.io.EOFException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.streams.io.SharpStream;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -121,7 +122,7 @@ public class WebMReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String readString(Element parent) throws IOException {
|
private String readString(Element parent) throws IOException {
|
||||||
return new String(readBlob(parent), "utf-8");
|
return new String(readBlob(parent), StandardCharsets.UTF_8);// or use "utf-8"
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] readBlob(Element parent) throws IOException {
|
private byte[] readBlob(Element parent) throws IOException {
|
||||||
@ -193,6 +194,7 @@ public class WebMReader {
|
|||||||
return elem;
|
return elem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ensure(elem);
|
ensure(elem);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -306,7 +308,7 @@ public class WebMReader {
|
|||||||
entry.trackNumber = readNumber(elem);
|
entry.trackNumber = readNumber(elem);
|
||||||
break;
|
break;
|
||||||
case ID_TrackType:
|
case ID_TrackType:
|
||||||
entry.trackType = (int)readNumber(elem);
|
entry.trackType = (int) readNumber(elem);
|
||||||
break;
|
break;
|
||||||
case ID_CodecID:
|
case ID_CodecID:
|
||||||
entry.codecId = readString(elem);
|
entry.codecId = readString(elem);
|
||||||
@ -445,7 +447,7 @@ public class WebMReader {
|
|||||||
|
|
||||||
public class SimpleBlock {
|
public class SimpleBlock {
|
||||||
|
|
||||||
public TrackDataChunk data;
|
public InputStream data;
|
||||||
|
|
||||||
SimpleBlock(Element ref) {
|
SimpleBlock(Element ref) {
|
||||||
this.ref = ref;
|
this.ref = ref;
|
||||||
@ -492,7 +494,7 @@ public class WebMReader {
|
|||||||
|
|
||||||
currentSimpleBlock = readSimpleBlock(elem);
|
currentSimpleBlock = readSimpleBlock(elem);
|
||||||
if (currentSimpleBlock.trackNumber == tracks[selectedTrack].trackNumber) {
|
if (currentSimpleBlock.trackNumber == tracks[selectedTrack].trackNumber) {
|
||||||
currentSimpleBlock.data = new TrackDataChunk(stream, (int) currentSimpleBlock.dataSize);
|
currentSimpleBlock.data = stream.getView((int) currentSimpleBlock.dataSize);
|
||||||
return currentSimpleBlock;
|
return currentSimpleBlock;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
package org.schabi.newpipe.streams;
|
package org.schabi.newpipe.streams;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
import org.schabi.newpipe.streams.WebMReader.Cluster;
|
import org.schabi.newpipe.streams.WebMReader.Cluster;
|
||||||
import org.schabi.newpipe.streams.WebMReader.Segment;
|
import org.schabi.newpipe.streams.WebMReader.Segment;
|
||||||
import org.schabi.newpipe.streams.WebMReader.SimpleBlock;
|
import org.schabi.newpipe.streams.WebMReader.SimpleBlock;
|
||||||
import org.schabi.newpipe.streams.WebMReader.WebMTrack;
|
import org.schabi.newpipe.streams.WebMReader.WebMTrack;
|
||||||
|
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.UnsupportedEncodingException;
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
import org.schabi.newpipe.streams.io.SharpStream;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @author kapodamy
|
* @author kapodamy
|
||||||
*/
|
*/
|
||||||
public class WebMWriter {
|
public class WebMWriter {
|
||||||
@ -94,10 +94,6 @@ public class WebMWriter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getBytesWritten() {
|
|
||||||
return written;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isDone() {
|
public boolean isDone() {
|
||||||
return done;
|
return done;
|
||||||
}
|
}
|
||||||
@ -111,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;
|
||||||
@ -138,42 +134,42 @@ public class WebMWriter {
|
|||||||
|
|
||||||
/* segment */
|
/* segment */
|
||||||
listBuffer.add(new byte[]{
|
listBuffer.add(new byte[]{
|
||||||
0x18, 0x53, (byte) 0x80, 0x67, 0x01,
|
0x18, 0x53, (byte) 0x80, 0x67, 0x01,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00// segment content size
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00// segment content size
|
||||||
});
|
});
|
||||||
|
|
||||||
long baseSegmentOffset = written + listBuffer.get(0).length;
|
long baseSegmentOffset = written + listBuffer.get(0).length;
|
||||||
|
|
||||||
/* seek head */
|
/* seek head */
|
||||||
listBuffer.add(new byte[]{
|
listBuffer.add(new byte[]{
|
||||||
0x11, 0x4d, (byte) 0x9b, 0x74, (byte) 0xbe,
|
0x11, 0x4d, (byte) 0x9b, 0x74, (byte) 0xbe,
|
||||||
0x4d, (byte) 0xbb, (byte) 0x8b,
|
0x4d, (byte) 0xbb, (byte) 0x8b,
|
||||||
0x53, (byte) 0xab, (byte) 0x84, 0x15, 0x49, (byte) 0xa9, 0x66, 0x53,
|
0x53, (byte) 0xab, (byte) 0x84, 0x15, 0x49, (byte) 0xa9, 0x66, 0x53,
|
||||||
(byte) 0xac, (byte) 0x81, /*info offset*/ 0x43,
|
(byte) 0xac, (byte) 0x81, /*info offset*/ 0x43,
|
||||||
0x4d, (byte) 0xbb, (byte) 0x8b, 0x53, (byte) 0xab,
|
0x4d, (byte) 0xbb, (byte) 0x8b, 0x53, (byte) 0xab,
|
||||||
(byte) 0x84, 0x16, 0x54, (byte) 0xae, 0x6b, 0x53, (byte) 0xac, (byte) 0x81,
|
(byte) 0x84, 0x16, 0x54, (byte) 0xae, 0x6b, 0x53, (byte) 0xac, (byte) 0x81,
|
||||||
/*tracks offset*/ 0x6a,
|
/*tracks offset*/ 0x6a,
|
||||||
0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1f,
|
0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1f,
|
||||||
0x43, (byte) 0xb6, 0x75, 0x53, (byte) 0xac, (byte) 0x84, /*cluster offset [2]*/ 0x00, 0x00, 0x00, 0x00,
|
0x43, (byte) 0xb6, 0x75, 0x53, (byte) 0xac, (byte) 0x84, /*cluster offset [2]*/ 0x00, 0x00, 0x00, 0x00,
|
||||||
0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1c, 0x53,
|
0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1c, 0x53,
|
||||||
(byte) 0xbb, 0x6b, 0x53, (byte) 0xac, (byte) 0x84, /*cues offset [7]*/ 0x00, 0x00, 0x00, 0x00
|
(byte) 0xbb, 0x6b, 0x53, (byte) 0xac, (byte) 0x84, /*cues offset [7]*/ 0x00, 0x00, 0x00, 0x00
|
||||||
});
|
});
|
||||||
|
|
||||||
/* info */
|
/* info */
|
||||||
listBuffer.add(new byte[]{
|
listBuffer.add(new byte[]{
|
||||||
0x15, 0x49, (byte) 0xa9, 0x66, (byte) 0xa2, 0x2a, (byte) 0xd7, (byte) 0xb1
|
0x15, 0x49, (byte) 0xa9, 0x66, (byte) 0xa2, 0x2a, (byte) 0xd7, (byte) 0xb1
|
||||||
});
|
});
|
||||||
listBuffer.add(encode(DEFAULT_TIMECODE_SCALE, true));// this value MUST NOT exceed 4 bytes
|
listBuffer.add(encode(DEFAULT_TIMECODE_SCALE, true));// this value MUST NOT exceed 4 bytes
|
||||||
listBuffer.add(new byte[]{0x44, (byte) 0x89, (byte) 0x84,
|
listBuffer.add(new byte[]{0x44, (byte) 0x89, (byte) 0x84,
|
||||||
0x00, 0x00, 0x00, 0x00,// info.duration
|
0x00, 0x00, 0x00, 0x00,// info.duration
|
||||||
|
|
||||||
/* MuxingApp */
|
/* MuxingApp */
|
||||||
0x4d, (byte) 0x80, (byte) 0x87, 0x4E,
|
0x4d, (byte) 0x80, (byte) 0x87, 0x4E,
|
||||||
0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string
|
0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string
|
||||||
|
|
||||||
/* WritingApp */
|
/* WritingApp */
|
||||||
0x57, 0x41, (byte) 0x87, 0x4E,
|
0x57, 0x41, (byte) 0x87, 0x4E,
|
||||||
0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string
|
0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string
|
||||||
});
|
});
|
||||||
|
|
||||||
/* tracks */
|
/* tracks */
|
||||||
@ -200,7 +196,6 @@ public class WebMWriter {
|
|||||||
long nextCueTime = infoTracks[cuesForTrackId].trackType == 1 ? -1 : 0;
|
long nextCueTime = infoTracks[cuesForTrackId].trackType == 1 ? -1 : 0;
|
||||||
ArrayList<KeyFrame> keyFrames = new ArrayList<>(32);
|
ArrayList<KeyFrame> keyFrames = new ArrayList<>(32);
|
||||||
|
|
||||||
//ArrayList<Block> chunks = new ArrayList<>(readers.length);
|
|
||||||
ArrayList<Long> clusterOffsets = new ArrayList<>(32);
|
ArrayList<Long> clusterOffsets = new ArrayList<>(32);
|
||||||
ArrayList<Integer> clusterSizes = new ArrayList<>(32);
|
ArrayList<Integer> clusterSizes = new ArrayList<>(32);
|
||||||
|
|
||||||
@ -283,24 +278,21 @@ public class WebMWriter {
|
|||||||
|
|
||||||
long segmentSize = written - offsetSegmentSizeSet - 7;
|
long segmentSize = written - offsetSegmentSizeSet - 7;
|
||||||
|
|
||||||
// final step write offsets and sizes
|
/* ---- final step write offsets and sizes ---- */
|
||||||
out.rewind();
|
seekTo(out, offsetSegmentSizeSet);
|
||||||
written = 0;
|
|
||||||
|
|
||||||
skipTo(out, offsetSegmentSizeSet);
|
|
||||||
writeLong(out, segmentSize);
|
writeLong(out, segmentSize);
|
||||||
|
|
||||||
if (predefinedDurations[durationFromTrackId] > -1) {
|
if (predefinedDurations[durationFromTrackId] > -1) {
|
||||||
duration += predefinedDurations[durationFromTrackId];// this value is full-filled in makeTrackEntry() method
|
duration += predefinedDurations[durationFromTrackId];// this value is full-filled in makeTrackEntry() method
|
||||||
}
|
}
|
||||||
skipTo(out, offsetInfoDurationSet);
|
seekTo(out, offsetInfoDurationSet);
|
||||||
writeFloat(out, duration);
|
writeFloat(out, duration);
|
||||||
|
|
||||||
firstClusterOffset -= baseSegmentOffset;
|
firstClusterOffset -= baseSegmentOffset;
|
||||||
skipTo(out, offsetClusterSet);
|
seekTo(out, offsetClusterSet);
|
||||||
writeInt(out, firstClusterOffset);
|
writeInt(out, firstClusterOffset);
|
||||||
|
|
||||||
skipTo(out, cueReservedOffset);
|
seekTo(out, cueReservedOffset);
|
||||||
|
|
||||||
/* Cue */
|
/* Cue */
|
||||||
dump(new byte[]{0x1c, 0x53, (byte) 0xbb, 0x6b, 0x20, 0x00, 0x00}, out);
|
dump(new byte[]{0x1c, 0x53, (byte) 0xbb, 0x6b, 0x20, 0x00, 0x00}, out);
|
||||||
@ -321,20 +313,16 @@ public class WebMWriter {
|
|||||||
voidBuffer.putShort((short) (firstClusterOffset - written - 4));
|
voidBuffer.putShort((short) (firstClusterOffset - written - 4));
|
||||||
dump(voidBuffer.array(), out);
|
dump(voidBuffer.array(), out);
|
||||||
|
|
||||||
out.rewind();
|
seekTo(out, offsetCuesSet);
|
||||||
written = 0;
|
|
||||||
|
|
||||||
skipTo(out, offsetCuesSet);
|
|
||||||
writeInt(out, (int) (cueReservedOffset - baseSegmentOffset));
|
writeInt(out, (int) (cueReservedOffset - baseSegmentOffset));
|
||||||
|
|
||||||
skipTo(out, cueReservedOffset + 5);
|
seekTo(out, cueReservedOffset + 5);
|
||||||
writeShort(out, cueSize);
|
writeShort(out, cueSize);
|
||||||
|
|
||||||
for (int i = 0; i < clusterSizes.size(); i++) {
|
for (int i = 0; i < clusterSizes.size(); i++) {
|
||||||
skipTo(out, clusterOffsets.get(i));
|
seekTo(out, clusterOffsets.get(i));
|
||||||
byte[] size = ByteBuffer.allocate(4).putInt(clusterSizes.get(i) | 0x200000).array();
|
byte[] buffer = ByteBuffer.allocate(4).putInt(clusterSizes.get(i) | 0x10000000).array();
|
||||||
out.write(size, 1, 3);
|
dump(buffer, out);
|
||||||
written += 3;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,20 +353,29 @@ public class WebMWriter {
|
|||||||
bloq.dataSize = (int) res.dataSize;
|
bloq.dataSize = (int) res.dataSize;
|
||||||
bloq.trackNumber = internalTrackId;
|
bloq.trackNumber = internalTrackId;
|
||||||
bloq.flags = res.flags;
|
bloq.flags = res.flags;
|
||||||
bloq.absoluteTimecode = convertTimecode(res.relativeTimeCode, readersSegment[internalTrackId].info.timecodeScale, DEFAULT_TIMECODE_SCALE);
|
bloq.absoluteTimecode = convertTimecode(res.relativeTimeCode, readersSegment[internalTrackId].info.timecodeScale);
|
||||||
bloq.absoluteTimecode += readersCluter[internalTrackId].timecode;
|
bloq.absoluteTimecode += readersCluter[internalTrackId].timecode;
|
||||||
|
|
||||||
return bloq;
|
return bloq;
|
||||||
}
|
}
|
||||||
|
|
||||||
private short convertTimecode(int time, long oldTimeScale, int newTimeScale) {
|
private short convertTimecode(int time, long oldTimeScale) {
|
||||||
return (short) (time * (newTimeScale / oldTimeScale));
|
return (short) (time * (DEFAULT_TIMECODE_SCALE / oldTimeScale));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void skipTo(SharpStream stream, long absoluteOffset) throws IOException {
|
private void seekTo(SharpStream stream, long offset) throws IOException {
|
||||||
absoluteOffset -= written;
|
if (stream.canSeek()) {
|
||||||
written += absoluteOffset;
|
stream.seek(offset);
|
||||||
stream.skip(absoluteOffset);
|
} else {
|
||||||
|
if (offset > written) {
|
||||||
|
stream.skip(offset - written);
|
||||||
|
} else {
|
||||||
|
stream.rewind();
|
||||||
|
stream.skip(offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
written = offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeLong(SharpStream stream, long number) throws IOException {
|
private void writeLong(SharpStream stream, long number) throws IOException {
|
||||||
@ -453,7 +450,7 @@ public class WebMWriter {
|
|||||||
/* cluster */
|
/* cluster */
|
||||||
dump(new byte[]{0x1f, 0x43, (byte) 0xb6, 0x75}, stream);
|
dump(new byte[]{0x1f, 0x43, (byte) 0xb6, 0x75}, stream);
|
||||||
clusterOffsets.add(written);// warning: max cluster size is 256 MiB
|
clusterOffsets.add(written);// warning: max cluster size is 256 MiB
|
||||||
dump(new byte[]{0x20, 0x00, 0x00}, stream);
|
dump(new byte[]{0x10, 0x00, 0x00, 0x00}, stream);
|
||||||
|
|
||||||
startOffset = written;// size for the this cluster
|
startOffset = written;// size for the this cluster
|
||||||
|
|
||||||
@ -468,12 +465,12 @@ public class WebMWriter {
|
|||||||
private void makeEBML(SharpStream stream) throws IOException {
|
private void makeEBML(SharpStream stream) throws IOException {
|
||||||
// deafult values
|
// deafult values
|
||||||
dump(new byte[]{
|
dump(new byte[]{
|
||||||
0x1A, 0x45, (byte) 0xDF, (byte) 0xA3, 0x01, 0x00, 0x00, 0x00,
|
0x1A, 0x45, (byte) 0xDF, (byte) 0xA3, 0x01, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x1F, 0x42, (byte) 0x86, (byte) 0x81, 0x01,
|
0x00, 0x00, 0x00, 0x1F, 0x42, (byte) 0x86, (byte) 0x81, 0x01,
|
||||||
0x42, (byte) 0xF7, (byte) 0x81, 0x01, 0x42, (byte) 0xF2, (byte) 0x81, 0x04,
|
0x42, (byte) 0xF7, (byte) 0x81, 0x01, 0x42, (byte) 0xF2, (byte) 0x81, 0x04,
|
||||||
0x42, (byte) 0xF3, (byte) 0x81, 0x08, 0x42, (byte) 0x82, (byte) 0x84, 0x77,
|
0x42, (byte) 0xF3, (byte) 0x81, 0x08, 0x42, (byte) 0x82, (byte) 0x84, 0x77,
|
||||||
0x65, 0x62, 0x6D, 0x42, (byte) 0x87, (byte) 0x81, 0x02,
|
0x65, 0x62, 0x6D, 0x42, (byte) 0x87, (byte) 0x81, 0x02,
|
||||||
0x42, (byte) 0x85, (byte) 0x81, 0x02
|
0x42, (byte) 0x85, (byte) 0x81, 0x02
|
||||||
}, stream);
|
}, stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -618,9 +615,10 @@ public class WebMWriter {
|
|||||||
|
|
||||||
int offset = withLength ? 1 : 0;
|
int offset = withLength ? 1 : 0;
|
||||||
byte[] buffer = new byte[offset + length];
|
byte[] buffer = new byte[offset + length];
|
||||||
long marker = (long) Math.floor((length - 1) / 8);
|
long marker = (long) Math.floor((length - 1f) / 8f);
|
||||||
|
|
||||||
for (int i = length - 1, mul = 1; i >= 0; i--, mul *= 0x100) {
|
float mul = 1;
|
||||||
|
for (int i = length - 1; i >= 0; i--, mul *= 0x100) {
|
||||||
long b = (long) Math.floor(number / mul);
|
long b = (long) Math.floor(number / mul);
|
||||||
if (!withLength && i == marker) {
|
if (!withLength && i == marker) {
|
||||||
b = b | (0x80 >> (length - 1));
|
b = b | (0x80 >> (length - 1));
|
||||||
@ -637,11 +635,7 @@ public class WebMWriter {
|
|||||||
|
|
||||||
private ArrayList<byte[]> encode(String value) {
|
private ArrayList<byte[]> encode(String value) {
|
||||||
byte[] str;
|
byte[] str;
|
||||||
try {
|
str = value.getBytes(StandardCharsets.UTF_8);// or use "utf-8"
|
||||||
str = value.getBytes("utf-8");
|
|
||||||
} catch (UnsupportedEncodingException err) {
|
|
||||||
str = value.getBytes();
|
|
||||||
}
|
|
||||||
|
|
||||||
ArrayList<byte[]> buffer = new ArrayList<>(2);
|
ArrayList<byte[]> buffer = new ArrayList<>(2);
|
||||||
buffer.add(encode(str.length, false));
|
buffer.add(encode(str.length, false));
|
||||||
@ -720,9 +714,10 @@ public class WebMWriter {
|
|||||||
return (flags & 0x80) == 0x80;
|
return (flags & 0x80) == 0x80;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return String.format("trackNumber=%s isKeyFrame=%S absoluteTimecode=%s", trackNumber, (flags & 0x80) == 0x80, absoluteTimecode);
|
return String.format("trackNumber=%s isKeyFrame=%S absoluteTimecode=%s", trackNumber, isKeyframe(), absoluteTimecode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 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;
|
||||||
|
|
||||||
@ -15,16 +16,14 @@ public abstract class SharpStream {
|
|||||||
|
|
||||||
public abstract long skip(long amount) throws IOException;
|
public abstract long skip(long amount) throws IOException;
|
||||||
|
|
||||||
|
public abstract long available();
|
||||||
public abstract int available();
|
|
||||||
|
|
||||||
public abstract void rewind() throws IOException;
|
public abstract void rewind() throws IOException;
|
||||||
|
|
||||||
|
public abstract boolean isClosed();
|
||||||
|
|
||||||
public abstract void dispose();
|
@Override
|
||||||
|
public abstract void close();
|
||||||
public abstract boolean isDisposed();
|
|
||||||
|
|
||||||
|
|
||||||
public abstract boolean canRewind();
|
public abstract boolean canRewind();
|
||||||
|
|
||||||
@ -32,6 +31,13 @@ public abstract class SharpStream {
|
|||||||
|
|
||||||
public abstract boolean canWrite();
|
public abstract boolean canWrite();
|
||||||
|
|
||||||
|
public boolean canSetLength() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canSeek() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public abstract void write(byte value) throws IOException;
|
public abstract void write(byte value) throws IOException;
|
||||||
|
|
||||||
@ -39,9 +45,19 @@ public abstract class SharpStream {
|
|||||||
|
|
||||||
public abstract void write(byte[] buffer, int offset, int count) throws IOException;
|
public abstract void write(byte[] buffer, int offset, int count) throws IOException;
|
||||||
|
|
||||||
public abstract void flush() throws IOException;
|
public void flush() throws IOException {
|
||||||
|
// STUB
|
||||||
|
}
|
||||||
|
|
||||||
public void setLength(long length) throws IOException {
|
public void setLength(long length) throws IOException {
|
||||||
throw new IOException("Not implemented");
|
throw new IOException("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void seek(long offset) throws IOException {
|
||||||
|
throw new IOException("Not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
public long length() throws IOException {
|
||||||
|
throw new UnsupportedOperationException("Unsupported operation");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,7 @@ public class CommentTextOnTouchListener implements View.OnTouchListener {
|
|||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(info -> {
|
.subscribe(info -> {
|
||||||
PlayQueue playQueue = new SinglePlayQueue((StreamInfo) info, seconds*1000);
|
PlayQueue playQueue = new SinglePlayQueue((StreamInfo) info, seconds*1000);
|
||||||
NavigationHelper.playOnPopupPlayer(context, playQueue);
|
NavigationHelper.playOnPopupPlayer(context, playQueue, false);
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,9 @@ import java.util.regex.Pattern;
|
|||||||
|
|
||||||
public class FilenameUtils {
|
public class FilenameUtils {
|
||||||
|
|
||||||
|
private static final String CHARSET_MOST_SPECIAL = "[\\n\\r|?*<\":\\\\>/']+";
|
||||||
|
private static final String CHARSET_ONLY_LETTERS_AND_DIGITS = "[^\\w\\d]+";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* #143 #44 #42 #22: make sure that the filename does not contain illegal chars.
|
* #143 #44 #42 #22: make sure that the filename does not contain illegal chars.
|
||||||
* @param context the context to retrieve strings and preferences from
|
* @param context the context to retrieve strings and preferences from
|
||||||
@ -18,11 +21,28 @@ public class FilenameUtils {
|
|||||||
*/
|
*/
|
||||||
public static String createFilename(Context context, String title) {
|
public static String createFilename(Context context, String title) {
|
||||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
final String key = context.getString(R.string.settings_file_charset_key);
|
|
||||||
final String value = sharedPreferences.getString(key, context.getString(R.string.default_file_charset_value));
|
final String charset_ld = context.getString(R.string.charset_letters_and_digits_value);
|
||||||
Pattern pattern = Pattern.compile(value);
|
final String charset_ms = context.getString(R.string.charset_most_special_value);
|
||||||
|
final String defaultCharset = context.getString(R.string.default_file_charset_value);
|
||||||
|
|
||||||
final String replacementChar = sharedPreferences.getString(context.getString(R.string.settings_file_replacement_character_key), "_");
|
final String replacementChar = sharedPreferences.getString(context.getString(R.string.settings_file_replacement_character_key), "_");
|
||||||
|
String selectedCharset = sharedPreferences.getString(context.getString(R.string.settings_file_charset_key), null);
|
||||||
|
|
||||||
|
final String charset;
|
||||||
|
|
||||||
|
if (selectedCharset == null || selectedCharset.isEmpty()) selectedCharset = defaultCharset;
|
||||||
|
|
||||||
|
if (selectedCharset.equals(charset_ld)) {
|
||||||
|
charset = CHARSET_ONLY_LETTERS_AND_DIGITS;
|
||||||
|
} else if (selectedCharset.equals(charset_ms)) {
|
||||||
|
charset = CHARSET_MOST_SPECIAL;
|
||||||
|
} else {
|
||||||
|
charset = selectedCharset;// ¿is the user using a custom charset?
|
||||||
|
}
|
||||||
|
|
||||||
|
Pattern pattern = Pattern.compile(charset);
|
||||||
|
|
||||||
return createFilename(title, pattern, replacementChar);
|
return createFilename(title, pattern, replacementChar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -430,24 +430,26 @@ public final class ListHelper {
|
|||||||
*/
|
*/
|
||||||
private static String getResolutionLimit(Context context) {
|
private static String getResolutionLimit(Context context) {
|
||||||
String resolutionLimit = null;
|
String resolutionLimit = null;
|
||||||
if (!isWifiActive(context)) {
|
if (isMeteredNetwork(context)) {
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
String defValue = context.getString(R.string.limit_data_usage_none_key);
|
String defValue = context.getString(R.string.limit_data_usage_none_key);
|
||||||
String value = preferences.getString(
|
String value = preferences.getString(
|
||||||
context.getString(R.string.limit_mobile_data_usage_key), defValue);
|
context.getString(R.string.limit_mobile_data_usage_key), defValue);
|
||||||
resolutionLimit = value.equals(defValue) ? null : value;
|
resolutionLimit = defValue.equals(value) ? null : value;
|
||||||
}
|
}
|
||||||
return resolutionLimit;
|
return resolutionLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Are we connected to wifi?
|
* The current network is metered (like mobile data)?
|
||||||
* @param context App context
|
* @param context App context
|
||||||
* @return {@code true} if connected to wifi
|
* @return {@code true} if connected to a metered network
|
||||||
*/
|
*/
|
||||||
private static boolean isWifiActive(Context context)
|
private static boolean isMeteredNetwork(Context context)
|
||||||
{
|
{
|
||||||
ConnectivityManager manager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
ConnectivityManager manager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
return manager != null && manager.getActiveNetworkInfo() != null && manager.getActiveNetworkInfo().getType() == ConnectivityManager.TYPE_WIFI;
|
if (manager == null || manager.getActiveNetworkInfo() == null) return false;
|
||||||
|
|
||||||
|
return manager.isActiveNetworkMetered();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,12 +69,14 @@ public class NavigationHelper {
|
|||||||
public static Intent getPlayerIntent(@NonNull final Context context,
|
public static Intent getPlayerIntent(@NonNull final Context context,
|
||||||
@NonNull final Class targetClazz,
|
@NonNull final Class targetClazz,
|
||||||
@NonNull final PlayQueue playQueue,
|
@NonNull final PlayQueue playQueue,
|
||||||
@Nullable final String quality) {
|
@Nullable final String quality,
|
||||||
|
final boolean resumePlayback) {
|
||||||
Intent intent = new Intent(context, targetClazz);
|
Intent intent = new Intent(context, targetClazz);
|
||||||
|
|
||||||
final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class);
|
final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class);
|
||||||
if (cacheKey != null) intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey);
|
if (cacheKey != null) intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey);
|
||||||
if (quality != null) intent.putExtra(VideoPlayer.PLAYBACK_QUALITY, quality);
|
if (quality != null) intent.putExtra(VideoPlayer.PLAYBACK_QUALITY, quality);
|
||||||
|
intent.putExtra(VideoPlayer.RESUME_PLAYBACK, resumePlayback);
|
||||||
|
|
||||||
return intent;
|
return intent;
|
||||||
}
|
}
|
||||||
@ -82,16 +84,18 @@ public class NavigationHelper {
|
|||||||
@NonNull
|
@NonNull
|
||||||
public static Intent getPlayerIntent(@NonNull final Context context,
|
public static Intent getPlayerIntent(@NonNull final Context context,
|
||||||
@NonNull final Class targetClazz,
|
@NonNull final Class targetClazz,
|
||||||
@NonNull final PlayQueue playQueue) {
|
@NonNull final PlayQueue playQueue,
|
||||||
return getPlayerIntent(context, targetClazz, playQueue, null);
|
final boolean resumePlayback) {
|
||||||
|
return getPlayerIntent(context, targetClazz, playQueue, null, resumePlayback);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public static Intent getPlayerEnqueueIntent(@NonNull final Context context,
|
public static Intent getPlayerEnqueueIntent(@NonNull final Context context,
|
||||||
@NonNull final Class targetClazz,
|
@NonNull final Class targetClazz,
|
||||||
@NonNull final PlayQueue playQueue,
|
@NonNull final PlayQueue playQueue,
|
||||||
final boolean selectOnAppend) {
|
final boolean selectOnAppend,
|
||||||
return getPlayerIntent(context, targetClazz, playQueue)
|
final boolean resumePlayback) {
|
||||||
|
return getPlayerIntent(context, targetClazz, playQueue, resumePlayback)
|
||||||
.putExtra(BasePlayer.APPEND_ONLY, true)
|
.putExtra(BasePlayer.APPEND_ONLY, true)
|
||||||
.putExtra(BasePlayer.SELECT_ON_APPEND, selectOnAppend);
|
.putExtra(BasePlayer.SELECT_ON_APPEND, selectOnAppend);
|
||||||
}
|
}
|
||||||
@ -104,40 +108,41 @@ public class NavigationHelper {
|
|||||||
final float playbackSpeed,
|
final float playbackSpeed,
|
||||||
final float playbackPitch,
|
final float playbackPitch,
|
||||||
final boolean playbackSkipSilence,
|
final boolean playbackSkipSilence,
|
||||||
@Nullable final String playbackQuality) {
|
@Nullable final String playbackQuality,
|
||||||
return getPlayerIntent(context, targetClazz, playQueue, playbackQuality)
|
final boolean resumePlayback) {
|
||||||
|
return getPlayerIntent(context, targetClazz, playQueue, playbackQuality, resumePlayback)
|
||||||
.putExtra(BasePlayer.REPEAT_MODE, repeatMode)
|
.putExtra(BasePlayer.REPEAT_MODE, repeatMode)
|
||||||
.putExtra(BasePlayer.PLAYBACK_SPEED, playbackSpeed)
|
.putExtra(BasePlayer.PLAYBACK_SPEED, playbackSpeed)
|
||||||
.putExtra(BasePlayer.PLAYBACK_PITCH, playbackPitch)
|
.putExtra(BasePlayer.PLAYBACK_PITCH, playbackPitch)
|
||||||
.putExtra(BasePlayer.PLAYBACK_SKIP_SILENCE, playbackSkipSilence);
|
.putExtra(BasePlayer.PLAYBACK_SKIP_SILENCE, playbackSkipSilence);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void playOnMainPlayer(final Context context, final PlayQueue queue) {
|
public static void playOnMainPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) {
|
||||||
final Intent playerIntent = getPlayerIntent(context, MainVideoPlayer.class, queue);
|
final Intent playerIntent = getPlayerIntent(context, MainVideoPlayer.class, queue, resumePlayback);
|
||||||
playerIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
playerIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
context.startActivity(playerIntent);
|
context.startActivity(playerIntent);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void playOnPopupPlayer(final Context context, final PlayQueue queue) {
|
public static void playOnPopupPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) {
|
||||||
if (!PermissionHelper.isPopupEnabled(context)) {
|
if (!PermissionHelper.isPopupEnabled(context)) {
|
||||||
PermissionHelper.showPopupEnablementToast(context);
|
PermissionHelper.showPopupEnablementToast(context);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
|
Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
|
||||||
startService(context, getPlayerIntent(context, PopupVideoPlayer.class, queue));
|
startService(context, getPlayerIntent(context, PopupVideoPlayer.class, queue, resumePlayback));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void playOnBackgroundPlayer(final Context context, final PlayQueue queue) {
|
public static void playOnBackgroundPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) {
|
||||||
Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT).show();
|
Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT).show();
|
||||||
startService(context, getPlayerIntent(context, BackgroundPlayer.class, queue));
|
startService(context, getPlayerIntent(context, BackgroundPlayer.class, queue, resumePlayback));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue) {
|
public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) {
|
||||||
enqueueOnPopupPlayer(context, queue, false);
|
enqueueOnPopupPlayer(context, queue, false, resumePlayback);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend) {
|
public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend, final boolean resumePlayback) {
|
||||||
if (!PermissionHelper.isPopupEnabled(context)) {
|
if (!PermissionHelper.isPopupEnabled(context)) {
|
||||||
PermissionHelper.showPopupEnablementToast(context);
|
PermissionHelper.showPopupEnablementToast(context);
|
||||||
return;
|
return;
|
||||||
@ -145,17 +150,17 @@ public class NavigationHelper {
|
|||||||
|
|
||||||
Toast.makeText(context, R.string.popup_playing_append, Toast.LENGTH_SHORT).show();
|
Toast.makeText(context, R.string.popup_playing_append, Toast.LENGTH_SHORT).show();
|
||||||
startService(context,
|
startService(context,
|
||||||
getPlayerEnqueueIntent(context, PopupVideoPlayer.class, queue, selectOnAppend));
|
getPlayerEnqueueIntent(context, PopupVideoPlayer.class, queue, selectOnAppend, resumePlayback));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue) {
|
public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) {
|
||||||
enqueueOnBackgroundPlayer(context, queue, false);
|
enqueueOnBackgroundPlayer(context, queue, false, resumePlayback);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend) {
|
public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend, final boolean resumePlayback) {
|
||||||
Toast.makeText(context, R.string.background_player_append, Toast.LENGTH_SHORT).show();
|
Toast.makeText(context, R.string.background_player_append, Toast.LENGTH_SHORT).show();
|
||||||
startService(context,
|
startService(context,
|
||||||
getPlayerEnqueueIntent(context, BackgroundPlayer.class, queue, selectOnAppend));
|
getPlayerEnqueueIntent(context, BackgroundPlayer.class, queue, selectOnAppend, resumePlayback));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void startService(@NonNull final Context context, @NonNull final Intent intent) {
|
public static void startService(@NonNull final Context context, @NonNull final Intent intent) {
|
||||||
|
@ -38,7 +38,7 @@ public class SecondaryStreamHelper<T extends Stream> {
|
|||||||
public static AudioStream getAudioStreamFor(@NonNull List<AudioStream> audioStreams, @NonNull VideoStream videoStream) {
|
public static AudioStream getAudioStreamFor(@NonNull List<AudioStream> audioStreams, @NonNull VideoStream videoStream) {
|
||||||
switch (videoStream.getFormat()) {
|
switch (videoStream.getFormat()) {
|
||||||
case WEBM:
|
case WEBM:
|
||||||
case MPEG_4:
|
case MPEG_4:// ¿is mpeg-4 DASH?
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
|
import android.util.SparseArray;
|
||||||
|
|
||||||
|
public abstract class SparseArrayUtils {
|
||||||
|
|
||||||
|
public static <T> void shiftItemsDown(SparseArray<T> sparseArray, int lower, int upper) {
|
||||||
|
for (int i = lower + 1; i <= upper; i++) {
|
||||||
|
final T o = sparseArray.get(i);
|
||||||
|
sparseArray.put(i - 1, o);
|
||||||
|
sparseArray.remove(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> void shiftItemsUp(SparseArray<T> sparseArray, int lower, int upper) {
|
||||||
|
for (int i = upper - 1; i >= lower; i--) {
|
||||||
|
final T o = sparseArray.get(i);
|
||||||
|
sparseArray.put(i + 1, o);
|
||||||
|
sparseArray.remove(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> int[] getKeys(SparseArray<T> sparseArray) {
|
||||||
|
final int[] result = new int[sparseArray.size()];
|
||||||
|
for (int i = 0; i < result.length; i++) {
|
||||||
|
result[i] = sparseArray.keyAt(i);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
package org.schabi.newpipe.views;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||||
|
import android.view.animation.Animation;
|
||||||
|
import android.view.animation.Transformation;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
|
||||||
|
public final class AnimatedProgressBar extends ProgressBar {
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private ProgressBarAnimation animation = null;
|
||||||
|
|
||||||
|
public AnimatedProgressBar(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AnimatedProgressBar(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AnimatedProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setProgressAnimated(int progress) {
|
||||||
|
cancelAnimation();
|
||||||
|
animation = new ProgressBarAnimation(this, getProgress(), progress);
|
||||||
|
startAnimation(animation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cancelAnimation() {
|
||||||
|
if (animation != null) {
|
||||||
|
animation.cancel();
|
||||||
|
animation = null;
|
||||||
|
}
|
||||||
|
clearAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ProgressBarAnimation extends Animation {
|
||||||
|
|
||||||
|
private final AnimatedProgressBar progressBar;
|
||||||
|
private final float from;
|
||||||
|
private final float to;
|
||||||
|
|
||||||
|
ProgressBarAnimation(AnimatedProgressBar progressBar, float from, float to) {
|
||||||
|
super();
|
||||||
|
this.progressBar = progressBar;
|
||||||
|
this.from = from;
|
||||||
|
this.to = to;
|
||||||
|
setDuration(500);
|
||||||
|
setInterpolator(new AccelerateDecelerateInterpolator());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void applyTransformation(float interpolatedTime, Transformation t) {
|
||||||
|
super.applyTransformation(interpolatedTime, t);
|
||||||
|
float value = from + (to - from) * interpolatedTime;
|
||||||
|
progressBar.setProgress((int) value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,185 +1,191 @@
|
|||||||
package us.shandian.giga.get;
|
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.InterruptedIOException;
|
import java.io.IOException;
|
||||||
import java.io.RandomAccessFile;
|
import java.io.InterruptedIOException;
|
||||||
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;
|
||||||
|
|
||||||
public class DownloadInitializer extends Thread {
|
public class DownloadInitializer extends Thread {
|
||||||
private final static String TAG = "DownloadInitializer";
|
private final static String TAG = "DownloadInitializer";
|
||||||
final static int mId = 0;
|
final static int mId = 0;
|
||||||
|
private final static int RESERVE_SPACE_DEFAULT = 5 * 1024 * 1024;// 5 MiB
|
||||||
private DownloadMission mMission;
|
private final static int RESERVE_SPACE_MAXIMUM = 150 * 1024 * 1024;// 150 MiB
|
||||||
private HttpURLConnection mConn;
|
|
||||||
|
private DownloadMission mMission;
|
||||||
DownloadInitializer(@NonNull DownloadMission mission) {
|
private HttpURLConnection mConn;
|
||||||
mMission = mission;
|
|
||||||
mConn = null;
|
DownloadInitializer(@NonNull DownloadMission mission) {
|
||||||
}
|
mMission = mission;
|
||||||
|
mConn = null;
|
||||||
@Override
|
}
|
||||||
public void run() {
|
|
||||||
if (mMission.current > 0) mMission.resetState();
|
@Override
|
||||||
|
public void run() {
|
||||||
int retryCount = 0;
|
if (mMission.current > 0) mMission.resetState(false, true, DownloadMission.ERROR_NOTHING);
|
||||||
while (true) {
|
|
||||||
try {
|
int retryCount = 0;
|
||||||
mMission.currentThreadCount = mMission.threadCount;
|
while (true) {
|
||||||
|
try {
|
||||||
mConn = mMission.openConnection(mId, -1, -1);
|
mMission.currentThreadCount = mMission.threadCount;
|
||||||
mMission.establishConnection(mId, mConn);
|
|
||||||
|
if (mMission.blocks < 0 && mMission.current == 0) {
|
||||||
if (!mMission.running || Thread.interrupted()) return;
|
// calculate the whole size of the mission
|
||||||
|
long finalLength = 0;
|
||||||
mMission.length = Utility.getContentLength(mConn);
|
long lowestSize = Long.MAX_VALUE;
|
||||||
|
|
||||||
|
for (int i = 0; i < mMission.urls.length && mMission.running; i++) {
|
||||||
if (mMission.length == 0) {
|
mConn = mMission.openConnection(mMission.urls[i], mId, -1, -1);
|
||||||
mMission.notifyError(DownloadMission.ERROR_HTTP_NO_CONTENT, null);
|
mMission.establishConnection(mId, mConn);
|
||||||
return;
|
|
||||||
}
|
if (Thread.interrupted()) return;
|
||||||
|
long length = Utility.getContentLength(mConn);
|
||||||
// check for dynamic generated content
|
|
||||||
if (mMission.length == -1 && mConn.getResponseCode() == 200) {
|
if (i == 0) mMission.length = length;
|
||||||
mMission.blocks = 0;
|
if (length > 0) finalLength += length;
|
||||||
mMission.length = 0;
|
if (length < lowestSize) lowestSize = length;
|
||||||
mMission.fallback = true;
|
}
|
||||||
mMission.unknownLength = true;
|
|
||||||
mMission.currentThreadCount = 1;
|
mMission.nearLength = finalLength;
|
||||||
|
|
||||||
if (DEBUG) {
|
// reserve space at the start of the file
|
||||||
Log.d(TAG, "falling back (unknown length)");
|
if (mMission.psAlgorithm != null && mMission.psAlgorithm.reserveSpace) {
|
||||||
}
|
if (lowestSize < 1) {
|
||||||
} else {
|
// the length is unknown use the default size
|
||||||
// Open again
|
mMission.offsets[0] = RESERVE_SPACE_DEFAULT;
|
||||||
mConn = mMission.openConnection(mId, mMission.length - 10, mMission.length);
|
} else {
|
||||||
mMission.establishConnection(mId, mConn);
|
// use the smallest resource size to download, otherwise, use the maximum
|
||||||
|
mMission.offsets[0] = lowestSize < RESERVE_SPACE_MAXIMUM ? lowestSize : RESERVE_SPACE_MAXIMUM;
|
||||||
if (!mMission.running || Thread.interrupted()) return;
|
}
|
||||||
|
}
|
||||||
synchronized (mMission.blockState) {
|
} else {
|
||||||
if (mConn.getResponseCode() == 206) {
|
// ask for the current resource length
|
||||||
if (mMission.currentThreadCount > 1) {
|
mConn = mMission.openConnection(mId, -1, -1);
|
||||||
mMission.blocks = mMission.length / DownloadMission.BLOCK_SIZE;
|
mMission.establishConnection(mId, mConn);
|
||||||
|
|
||||||
if (mMission.currentThreadCount > mMission.blocks) {
|
if (!mMission.running || Thread.interrupted()) return;
|
||||||
mMission.currentThreadCount = (int) mMission.blocks;
|
|
||||||
}
|
mMission.length = Utility.getContentLength(mConn);
|
||||||
if (mMission.currentThreadCount <= 0) {
|
}
|
||||||
mMission.currentThreadCount = 1;
|
|
||||||
}
|
if (mMission.length == 0 || mConn.getResponseCode() == 204) {
|
||||||
if (mMission.blocks * DownloadMission.BLOCK_SIZE < mMission.length) {
|
mMission.notifyError(DownloadMission.ERROR_HTTP_NO_CONTENT, null);
|
||||||
mMission.blocks++;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// if one thread is solicited don't calculate blocks, is useless
|
// check for dynamic generated content
|
||||||
mMission.blocks = 1;
|
if (mMission.length == -1 && mConn.getResponseCode() == 200) {
|
||||||
mMission.fallback = true;
|
mMission.blocks = 0;
|
||||||
mMission.unknownLength = false;
|
mMission.length = 0;
|
||||||
}
|
mMission.fallback = true;
|
||||||
|
mMission.unknownLength = true;
|
||||||
if (DEBUG) {
|
mMission.currentThreadCount = 1;
|
||||||
Log.d(TAG, "http response code = " + mConn.getResponseCode());
|
|
||||||
}
|
if (DEBUG) {
|
||||||
} else {
|
Log.d(TAG, "falling back (unknown length)");
|
||||||
// Fallback to single thread
|
}
|
||||||
mMission.blocks = 0;
|
} else {
|
||||||
mMission.fallback = true;
|
// Open again
|
||||||
mMission.unknownLength = false;
|
mConn = mMission.openConnection(mId, mMission.length - 10, mMission.length);
|
||||||
mMission.currentThreadCount = 1;
|
mMission.establishConnection(mId, mConn);
|
||||||
|
|
||||||
if (DEBUG) {
|
if (!mMission.running || Thread.interrupted()) return;
|
||||||
Log.d(TAG, "falling back due http response code = " + mConn.getResponseCode());
|
|
||||||
}
|
synchronized (mMission.blockState) {
|
||||||
}
|
if (mConn.getResponseCode() == 206) {
|
||||||
|
if (mMission.currentThreadCount > 1) {
|
||||||
for (long i = 0; i < mMission.currentThreadCount; i++) {
|
mMission.blocks = mMission.length / DownloadMission.BLOCK_SIZE;
|
||||||
mMission.threadBlockPositions.add(i);
|
|
||||||
mMission.threadBytePositions.add(0L);
|
if (mMission.currentThreadCount > mMission.blocks) {
|
||||||
}
|
mMission.currentThreadCount = (int) mMission.blocks;
|
||||||
}
|
}
|
||||||
|
if (mMission.currentThreadCount <= 0) {
|
||||||
if (!mMission.running || Thread.interrupted()) return;
|
mMission.currentThreadCount = 1;
|
||||||
}
|
}
|
||||||
|
if (mMission.blocks * DownloadMission.BLOCK_SIZE < mMission.length) {
|
||||||
File file;
|
mMission.blocks++;
|
||||||
if (mMission.current == 0) {
|
}
|
||||||
file = new File(mMission.location);
|
} else {
|
||||||
if (!Utility.mkdir(file, true)) {
|
// if one thread is solicited don't calculate blocks, is useless
|
||||||
mMission.notifyError(DownloadMission.ERROR_PATH_CREATION, null);
|
mMission.blocks = 1;
|
||||||
return;
|
mMission.fallback = true;
|
||||||
}
|
mMission.unknownLength = false;
|
||||||
|
}
|
||||||
file = new File(file, mMission.name);
|
|
||||||
|
if (DEBUG) {
|
||||||
// if the name is used by another process, delete it
|
Log.d(TAG, "http response code = " + mConn.getResponseCode());
|
||||||
if (file.exists() && !file.isFile() && !file.delete()) {
|
}
|
||||||
mMission.notifyError(DownloadMission.ERROR_FILE_CREATION, null);
|
} else {
|
||||||
return;
|
// Fallback to single thread
|
||||||
}
|
mMission.blocks = 0;
|
||||||
|
mMission.fallback = true;
|
||||||
if (!file.exists() && !file.createNewFile()) {
|
mMission.unknownLength = false;
|
||||||
mMission.notifyError(DownloadMission.ERROR_FILE_CREATION, null);
|
mMission.currentThreadCount = 1;
|
||||||
return;
|
|
||||||
}
|
if (DEBUG) {
|
||||||
} else {
|
Log.d(TAG, "falling back due http response code = " + mConn.getResponseCode());
|
||||||
file = new File(mMission.location, mMission.name);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RandomAccessFile af = new RandomAccessFile(file, "rw");
|
for (long i = 0; i < mMission.currentThreadCount; i++) {
|
||||||
af.setLength(mMission.offsets[mMission.current] + mMission.length);
|
mMission.threadBlockPositions.add(i);
|
||||||
af.seek(mMission.offsets[mMission.current]);
|
mMission.threadBytePositions.add(0L);
|
||||||
af.close();
|
}
|
||||||
|
}
|
||||||
if (!mMission.running || Thread.interrupted()) return;
|
|
||||||
|
if (!mMission.running || Thread.interrupted()) return;
|
||||||
mMission.running = false;
|
}
|
||||||
break;
|
|
||||||
} catch (InterruptedIOException | ClosedByInterruptException e) {
|
SharpStream fs = mMission.storage.getStream();
|
||||||
return;
|
fs.setLength(mMission.offsets[mMission.current] + mMission.length);
|
||||||
} catch (Exception e) {
|
fs.seek(mMission.offsets[mMission.current]);
|
||||||
if (!mMission.running) return;
|
fs.close();
|
||||||
|
|
||||||
if (e instanceof IOException && e.getMessage().contains("Permission denied")) {
|
if (!mMission.running || Thread.interrupted()) return;
|
||||||
mMission.notifyError(DownloadMission.ERROR_PERMISSION_DENIED, e);
|
|
||||||
return;
|
mMission.running = false;
|
||||||
}
|
break;
|
||||||
|
} catch (InterruptedIOException | ClosedByInterruptException e) {
|
||||||
if (retryCount++ > mMission.maxRetry) {
|
return;
|
||||||
Log.e(TAG, "initializer failed", e);
|
} catch (Exception e) {
|
||||||
mMission.notifyError(e);
|
if (!mMission.running) return;
|
||||||
return;
|
|
||||||
}
|
if (e instanceof IOException && e.getMessage().contains("Permission denied")) {
|
||||||
|
mMission.notifyError(DownloadMission.ERROR_PERMISSION_DENIED, e);
|
||||||
Log.e(TAG, "initializer failed, retrying", e);
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if (retryCount++ > mMission.maxRetry) {
|
||||||
// hide marquee in the progress bar
|
Log.e(TAG, "initializer failed", e);
|
||||||
mMission.done++;
|
mMission.notifyError(e);
|
||||||
|
return;
|
||||||
mMission.start();
|
}
|
||||||
}
|
|
||||||
|
Log.e(TAG, "initializer failed, retrying", e);
|
||||||
@Override
|
}
|
||||||
public void interrupt() {
|
}
|
||||||
super.interrupt();
|
|
||||||
|
mMission.start();
|
||||||
if (mConn != null) {
|
}
|
||||||
try {
|
|
||||||
mConn.disconnect();
|
@Override
|
||||||
} catch (Exception e) {
|
public void interrupt() {
|
||||||
// nothing to do
|
super.interrupt();
|
||||||
}
|
|
||||||
}
|
if (mConn != null) {
|
||||||
}
|
try {
|
||||||
}
|
mConn.disconnect();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -4,11 +4,14 @@ import android.os.Handler;
|
|||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.Downloader;
|
||||||
|
|
||||||
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.net.ConnectException;
|
import java.net.ConnectException;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.SocketTimeoutException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -17,6 +20,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 +28,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;
|
||||||
@ -40,6 +44,11 @@ public class DownloadMission extends Mission {
|
|||||||
public static final int ERROR_UNKNOWN_HOST = 1005;
|
public static final int ERROR_UNKNOWN_HOST = 1005;
|
||||||
public static final int ERROR_CONNECT_HOST = 1006;
|
public static final int ERROR_CONNECT_HOST = 1006;
|
||||||
public static final int ERROR_POSTPROCESSING = 1007;
|
public static final int ERROR_POSTPROCESSING = 1007;
|
||||||
|
public static final int ERROR_POSTPROCESSING_STOPPED = 1008;
|
||||||
|
public static final int ERROR_POSTPROCESSING_HOLD = 1009;
|
||||||
|
public static final int ERROR_INSUFFICIENT_STORAGE = 1010;
|
||||||
|
public static final int ERROR_PROGRESS_LOST = 1011;
|
||||||
|
public static final int ERROR_TIMEOUT = 1012;
|
||||||
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;
|
||||||
|
|
||||||
@ -68,43 +77,34 @@ 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
|
||||||
* 1: running
|
* 1: running
|
||||||
* 2: completed
|
* 2: completed
|
||||||
|
* 3: hold
|
||||||
*/
|
*/
|
||||||
public 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 Postprocessing psAlgorithm;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current resource to download {@code urls[current]}
|
* The current resource to download, see {@code urls[current]} and {@code offsets[current]}
|
||||||
*/
|
*/
|
||||||
public int current;
|
public int current;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Metadata where the mission state is saved
|
* Metadata where the mission state is saved
|
||||||
*/
|
*/
|
||||||
public File metadata;
|
public transient File metadata;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* maximum attempts
|
* maximum attempts
|
||||||
*/
|
*/
|
||||||
public int maxRetry;
|
public transient int maxRetry;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Approximated final length, this represent the sum of all resources sizes
|
* Approximated final length, this represent the sum of all resources sizes
|
||||||
@ -115,11 +115,11 @@ public class DownloadMission extends Mission {
|
|||||||
boolean fallback;
|
boolean fallback;
|
||||||
private int finishCount;
|
private int finishCount;
|
||||||
public transient boolean running;
|
public transient boolean running;
|
||||||
public transient boolean enqueued = true;
|
public boolean enqueued;
|
||||||
|
|
||||||
public int errCode = ERROR_NOTHING;
|
public int errCode = ERROR_NOTHING;
|
||||||
|
|
||||||
public transient Exception errObject = null;
|
public Exception errObject = null;
|
||||||
public transient boolean recovered;
|
public transient boolean recovered;
|
||||||
public transient Handler mHandler;
|
public transient Handler mHandler;
|
||||||
private transient boolean mWritingToFile;
|
private transient boolean mWritingToFile;
|
||||||
@ -131,41 +131,26 @@ public class DownloadMission extends Mission {
|
|||||||
|
|
||||||
private transient boolean deleted;
|
private transient boolean deleted;
|
||||||
int currentThreadCount;
|
int currentThreadCount;
|
||||||
private transient 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.maxRetry = 3;
|
||||||
|
this.storage = storage;
|
||||||
|
this.psAlgorithm = psInstance;
|
||||||
|
|
||||||
if (postprocessingName != null) {
|
if (DEBUG && psInstance == null && urls.length > 1) {
|
||||||
Postprocessing algorithm = Postprocessing.getAlgorithm(postprocessingName, null);
|
Log.w(TAG, "mission created with multiple urls ¿missing post-processing algorithm?");
|
||||||
this.postprocessingThis = algorithm.worksOnSameFile;
|
|
||||||
this.offsets[0] = algorithm.recommendedReserve;
|
|
||||||
this.postprocessingName = postprocessingName;
|
|
||||||
this.postprocessingArgs = postprocessingArgs;
|
|
||||||
} else {
|
|
||||||
if (DEBUG && urls.length > 1) {
|
|
||||||
Log.w(TAG, "mission created with multiple urls ¿missing post-processing algorithm?");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,6 +168,7 @@ public class DownloadMission extends Mission {
|
|||||||
*/
|
*/
|
||||||
boolean isBlockPreserved(long block) {
|
boolean isBlockPreserved(long block) {
|
||||||
checkBlock(block);
|
checkBlock(block);
|
||||||
|
//noinspection ConstantConditions
|
||||||
return blockState.containsKey(block) ? blockState.get(block) : false;
|
return blockState.containsKey(block) ? blockState.get(block) : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,9 +229,18 @@ public class DownloadMission extends Mission {
|
|||||||
* @throws IOException if an I/O exception occurs.
|
* @throws IOException if an I/O exception occurs.
|
||||||
*/
|
*/
|
||||||
HttpURLConnection openConnection(int threadId, long rangeStart, long rangeEnd) throws IOException {
|
HttpURLConnection openConnection(int threadId, long rangeStart, long rangeEnd) throws IOException {
|
||||||
URL url = new URL(urls[current]);
|
return openConnection(urls[current], threadId, rangeStart, rangeEnd);
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
}
|
||||||
|
|
||||||
|
HttpURLConnection openConnection(String url, int threadId, long rangeStart, long rangeEnd) throws IOException {
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
|
||||||
conn.setInstanceFollowRedirects(true);
|
conn.setInstanceFollowRedirects(true);
|
||||||
|
conn.setRequestProperty("User-Agent", Downloader.USER_AGENT);
|
||||||
|
conn.setRequestProperty("Accept", "*/*");
|
||||||
|
|
||||||
|
// BUG workaround: switching between networks can freeze the download forever
|
||||||
|
conn.setConnectTimeout(30000);
|
||||||
|
conn.setReadTimeout(10000);
|
||||||
|
|
||||||
if (rangeStart >= 0) {
|
if (rangeStart >= 0) {
|
||||||
String req = "bytes=" + rangeStart + "-";
|
String req = "bytes=" + rangeStart + "-";
|
||||||
@ -337,17 +332,42 @@ public class DownloadMission extends Mission {
|
|||||||
notifyError(ERROR_CONNECT_HOST, null);
|
notifyError(ERROR_CONNECT_HOST, null);
|
||||||
} else if (err instanceof UnknownHostException) {
|
} else if (err instanceof UnknownHostException) {
|
||||||
notifyError(ERROR_UNKNOWN_HOST, null);
|
notifyError(ERROR_UNKNOWN_HOST, null);
|
||||||
|
} else if (err instanceof SocketTimeoutException) {
|
||||||
|
notifyError(ERROR_TIMEOUT, null);
|
||||||
} else {
|
} else {
|
||||||
notifyError(ERROR_UNKNOWN_EXCEPTION, err);
|
notifyError(ERROR_UNKNOWN_EXCEPTION, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized void notifyError(int code, Exception err) {
|
public synchronized void notifyError(int code, Exception err) {
|
||||||
Log.e(TAG, "notifyError() code = " + code, err);
|
Log.e(TAG, "notifyError() code = " + code, err);
|
||||||
|
|
||||||
|
if (err instanceof IOException) {
|
||||||
|
if (!storage.canWrite() || err.getMessage().contains("Permission denied")) {
|
||||||
|
code = ERROR_PERMISSION_DENIED;
|
||||||
|
err = null;
|
||||||
|
} else if (err.getMessage().contains("ENOSPC")) {
|
||||||
|
code = ERROR_INSUFFICIENT_STORAGE;
|
||||||
|
err = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
errCode = code;
|
errCode = code;
|
||||||
errObject = err;
|
errObject = err;
|
||||||
|
|
||||||
|
switch (code) {
|
||||||
|
case ERROR_SSL_EXCEPTION:
|
||||||
|
case ERROR_UNKNOWN_HOST:
|
||||||
|
case ERROR_CONNECT_HOST:
|
||||||
|
case ERROR_TIMEOUT:
|
||||||
|
// do not change the queue flag for network errors, can be
|
||||||
|
// recovered silently without the user interaction
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// also checks for server errors
|
||||||
|
if (code < 500 || code > 599) enqueued = false;
|
||||||
|
}
|
||||||
|
|
||||||
pause();
|
pause();
|
||||||
|
|
||||||
notify(DownloadManagerService.MESSAGE_ERROR);
|
notify(DownloadManagerService.MESSAGE_ERROR);
|
||||||
@ -378,6 +398,7 @@ public class DownloadMission extends Mission {
|
|||||||
|
|
||||||
if (!doPostprocessing()) return;
|
if (!doPostprocessing()) return;
|
||||||
|
|
||||||
|
enqueued = false;
|
||||||
running = false;
|
running = false;
|
||||||
deleteThisFromFile();
|
deleteThisFromFile();
|
||||||
|
|
||||||
@ -386,25 +407,23 @@ public class DownloadMission extends Mission {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void notifyPostProcessing(int state) {
|
private void notifyPostProcessing(int state) {
|
||||||
if (DEBUG) {
|
String action;
|
||||||
String action;
|
switch (state) {
|
||||||
switch (state) {
|
case 1:
|
||||||
case 1:
|
action = "Running";
|
||||||
action = "Running";
|
break;
|
||||||
break;
|
case 2:
|
||||||
case 2:
|
action = "Completed";
|
||||||
action = "Completed";
|
break;
|
||||||
break;
|
default:
|
||||||
default:
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -420,11 +439,10 @@ public class DownloadMission extends Mission {
|
|||||||
if (threads != null)
|
if (threads != null)
|
||||||
for (Thread thread : threads) joinForThread(thread);
|
for (Thread thread : threads) joinForThread(thread);
|
||||||
|
|
||||||
enqueued = false;
|
|
||||||
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;
|
||||||
@ -463,7 +481,7 @@ public class DownloadMission extends Mission {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pause the mission, does not affect the blocks that are being downloaded.
|
* Pause the mission
|
||||||
*/
|
*/
|
||||||
public synchronized void pause() {
|
public synchronized void pause() {
|
||||||
if (!running) return;
|
if (!running) return;
|
||||||
@ -477,12 +495,11 @@ public class DownloadMission extends Mission {
|
|||||||
|
|
||||||
running = false;
|
running = false;
|
||||||
recovered = true;
|
recovered = true;
|
||||||
enqueued = false;
|
|
||||||
|
|
||||||
if (init != null && Thread.currentThread() != init && init.isAlive()) {
|
if (init != null && Thread.currentThread() != init && init.isAlive()) {
|
||||||
init.interrupt();
|
init.interrupt();
|
||||||
synchronized (blockState) {
|
synchronized (blockState) {
|
||||||
resetState();
|
resetState(false, true, ERROR_NOTHING);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -514,20 +531,31 @@ public class DownloadMission extends Mission {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the file and the meta file
|
* Removes the downloaded file and the meta file
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean delete() {
|
public boolean delete() {
|
||||||
deleted = true;
|
deleted = true;
|
||||||
|
if (psAlgorithm != null) psAlgorithm.cleanupTemporalDir();
|
||||||
|
|
||||||
boolean res = deleteThisFromFile();
|
boolean res = deleteThisFromFile();
|
||||||
if (!super.delete()) res = false;
|
|
||||||
|
if (!super.delete()) return false;
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
void resetState() {
|
|
||||||
|
/**
|
||||||
|
* Resets the mission state
|
||||||
|
*
|
||||||
|
* @param rollback {@code true} true to forget all progress, otherwise, {@code false}
|
||||||
|
* @param persistChanges {@code true} to commit changes to the metadata file, otherwise, {@code false}
|
||||||
|
*/
|
||||||
|
public void resetState(boolean rollback, boolean persistChanges, int errorCode) {
|
||||||
done = 0;
|
done = 0;
|
||||||
blocks = -1;
|
blocks = -1;
|
||||||
errCode = ERROR_NOTHING;
|
errCode = errorCode;
|
||||||
|
errObject = null;
|
||||||
fallback = false;
|
fallback = false;
|
||||||
unknownLength = false;
|
unknownLength = false;
|
||||||
finishCount = 0;
|
finishCount = 0;
|
||||||
@ -536,7 +564,10 @@ public class DownloadMission extends Mission {
|
|||||||
blockState.clear();
|
blockState.clear();
|
||||||
threads = new Thread[0];
|
threads = new Thread[0];
|
||||||
|
|
||||||
Utility.writeToFile(metadata, DownloadMission.this);
|
if (rollback) current = 0;
|
||||||
|
|
||||||
|
if (persistChanges)
|
||||||
|
Utility.writeToFile(metadata, DownloadMission.this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializer() {
|
private void initializer() {
|
||||||
@ -562,7 +593,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -571,7 +602,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -580,12 +617,26 @@ public class DownloadMission extends Mission {
|
|||||||
* @return true, otherwise, false
|
* @return true, otherwise, false
|
||||||
*/
|
*/
|
||||||
public boolean isPsRunning() {
|
public boolean isPsRunning() {
|
||||||
return postprocessingName != null && postprocessingState == 1;
|
return psAlgorithm != null && (psState == 1 || psState == 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicated if the mission is ready
|
||||||
|
*
|
||||||
|
* @return true, otherwise, false
|
||||||
|
*/
|
||||||
|
public boolean isInitialized() {
|
||||||
|
return blocks >= 0; // DownloadMissionInitializer was executed
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the approximated final length of the file
|
||||||
|
*
|
||||||
|
* @return the length in bytes
|
||||||
|
*/
|
||||||
public long getLength() {
|
public long getLength() {
|
||||||
long calculated;
|
long calculated;
|
||||||
if (postprocessingState == 1) {
|
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;
|
||||||
@ -596,30 +647,67 @@ public class DownloadMission extends Mission {
|
|||||||
return calculated > nearLength ? calculated : nearLength;
|
return calculated > nearLength ? calculated : nearLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set this mission state on the queue
|
||||||
|
*
|
||||||
|
* @param queue true to add to the queue, otherwise, false
|
||||||
|
*/
|
||||||
|
public void setEnqueued(boolean queue) {
|
||||||
|
enqueued = queue;
|
||||||
|
runAsync(-2, this::writeThisToFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to continue a blocked post-processing
|
||||||
|
*
|
||||||
|
* @param recover {@code true} to retry, otherwise, {@code false} to cancel
|
||||||
|
*/
|
||||||
|
public void psContinue(boolean recover) {
|
||||||
|
psState = 1;
|
||||||
|
errCode = recover ? ERROR_NOTHING : ERROR_POSTPROCESSING;
|
||||||
|
threads[0].interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() || !storage.existsAsFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whatever is possible to start the mission
|
||||||
|
*
|
||||||
|
* @return {@code true} is this mission its "healthy", otherwise, {@code false}
|
||||||
|
*/
|
||||||
|
public boolean isCorrupt() {
|
||||||
|
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;
|
||||||
|
|
||||||
|
errObject = null;
|
||||||
|
|
||||||
notifyPostProcessing(1);
|
notifyPostProcessing(1);
|
||||||
notifyProgress(0);
|
notifyProgress(0);
|
||||||
|
|
||||||
Thread.currentThread().setName("[" + TAG + "] post-processing = " + postprocessingName + " filename = " + name);
|
if (DEBUG)
|
||||||
|
Thread.currentThread().setName("[" + TAG + "] ps = " +
|
||||||
|
psAlgorithm.getClass().getSimpleName() +
|
||||||
|
" filename = " + storage.getName()
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
@ -669,7 +757,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;
|
||||||
|
|
||||||
@ -26,7 +27,6 @@ public class DownloadRunnable extends Thread {
|
|||||||
if (mission == null) throw new NullPointerException("mission is null");
|
if (mission == null) throw new NullPointerException("mission is null");
|
||||||
mMission = mission;
|
mMission = mission;
|
||||||
mId = id;
|
mId = id;
|
||||||
mConn = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -40,12 +40,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;
|
||||||
}
|
}
|
||||||
@ -136,6 +136,10 @@ public class DownloadRunnable extends Thread {
|
|||||||
mMission.setThreadBytePosition(mId, total);// download paused, save progress for this block
|
mMission.setThreadBytePosition(mId, total);// download paused, save progress for this block
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, mId + ": position=" + blockPosition + " total=" + total + " stopped due exception", e);
|
||||||
|
}
|
||||||
|
|
||||||
mMission.setThreadBytePosition(mId, total);
|
mMission.setThreadBytePosition(mId, total);
|
||||||
|
|
||||||
if (!mMission.running || e instanceof ClosedByInterruptException) break;
|
if (!mMission.running || e instanceof ClosedByInterruptException) break;
|
||||||
@ -145,10 +149,6 @@ public class DownloadRunnable extends Thread {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, mId + ":position " + blockPosition + " retrying due exception", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
retry = true;
|
retry = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
package us.shandian.giga.get;
|
package us.shandian.giga.get;
|
||||||
|
|
||||||
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;
|
||||||
@ -19,21 +18,17 @@ import static org.schabi.newpipe.BuildConfig.DEBUG;
|
|||||||
* Single-threaded fallback mode
|
* Single-threaded fallback mode
|
||||||
*/
|
*/
|
||||||
public class DownloadRunnableFallback extends Thread {
|
public class DownloadRunnableFallback extends Thread {
|
||||||
private static final String TAG = "DownloadRunnableFallback";
|
private static final String TAG = "DownloadRunnableFallbac";
|
||||||
|
|
||||||
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) {
|
||||||
mMission = mission;
|
mMission = mission;
|
||||||
mIs = null;
|
|
||||||
mF = null;
|
|
||||||
mConn = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void dispose() {
|
private void dispose() {
|
||||||
@ -43,15 +38,10 @@ 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
|
||||||
@SuppressLint("LongLogTag")
|
|
||||||
public void run() {
|
public void run() {
|
||||||
boolean done;
|
boolean done;
|
||||||
|
|
||||||
@ -67,6 +57,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 +72,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();
|
||||||
@ -110,6 +101,10 @@ public class DownloadRunnableFallback extends Thread {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.e(TAG, "got exception, retrying...", e);
|
||||||
|
}
|
||||||
|
|
||||||
run();// try again
|
run();// try again
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
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;
|
||||||
|
storage = mission.storage;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
package us.shandian.giga.get;
|
package us.shandian.giga.get;
|
||||||
|
|
||||||
import java.io.File;
|
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,33 +25,24 @@ 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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get the target file on the storage
|
* The downloaded file
|
||||||
*
|
|
||||||
* @return File object
|
|
||||||
*/
|
*/
|
||||||
public File getDownloadedFile() {
|
public StoredFileHelper storage;
|
||||||
return new File(location, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the downloaded file
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -57,10 +50,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() + "] " + (storage.isInvalid() ? storage.getName() : storage.getUri());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,237 @@
|
|||||||
|
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_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_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_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,null, Uri.parse(path), "");
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("FinishedMissionStore", "failed to load the storage path of: " + path, e);
|
||||||
|
mission.storage = new StoredFileHelper(null, path, "", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
return mission;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//////////////////////////////////
|
||||||
|
// Data source methods
|
||||||
|
///////////////////////////////////
|
||||||
|
|
||||||
|
public ArrayList<FinishedMission> loadFinishedMissions() {
|
||||||
|
SQLiteDatabase database = getReadableDatabase();
|
||||||
|
Cursor cursor = database.query(FINISHED_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_TABLE_NAME, null, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteMission(Mission mission) {
|
||||||
|
if (mission == null) throw new NullPointerException("mission is null");
|
||||||
|
String ts = String.valueOf(mission.timestamp);
|
||||||
|
|
||||||
|
SQLiteDatabase database = getWritableDatabase();
|
||||||
|
|
||||||
|
if (mission instanceof FinishedMission) {
|
||||||
|
if (mission.storage.isInvalid()) {
|
||||||
|
database.delete(FINISHED_TABLE_NAME, KEY_TIMESTAMP + " = ?", new String[]{ts});
|
||||||
|
} else {
|
||||||
|
database.delete(FINISHED_TABLE_NAME, KEY_TIMESTAMP + " = ? AND " + KEY_PATH + " = ?", new String[]{
|
||||||
|
ts, mission.storage.getUri().toString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} 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 ts = String.valueOf(mission.timestamp);
|
||||||
|
|
||||||
|
int rowsAffected;
|
||||||
|
|
||||||
|
if (mission instanceof FinishedMission) {
|
||||||
|
if (mission.storage.isInvalid()) {
|
||||||
|
rowsAffected = database.update(FINISHED_TABLE_NAME, values, KEY_TIMESTAMP + " = ?", new String[]{ts});
|
||||||
|
} else {
|
||||||
|
rowsAffected = database.update(FINISHED_TABLE_NAME, values, KEY_PATH + " = ?", new String[]{
|
||||||
|
mission.storage.getUri().toString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new UnsupportedOperationException("DownloadMission");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rowsAffected != 1) {
|
||||||
|
Log.e("FinishedMissionStore", "Expected 1 row to be affected by update but got " + rowsAffected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,153 +1,148 @@
|
|||||||
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 SharpStream source;
|
||||||
|
private final long offset;
|
||||||
private RandomAccessFile source;
|
private final long length;
|
||||||
private final long offset;
|
private long position;
|
||||||
private final long length;
|
|
||||||
private long position;
|
public ChunkFileInputStream(SharpStream target, long start) throws IOException {
|
||||||
|
this(target, start, target.length());
|
||||||
public ChunkFileInputStream(File file, long start, long end, String mode) throws IOException {
|
}
|
||||||
source = new RandomAccessFile(file, mode);
|
|
||||||
offset = start;
|
public ChunkFileInputStream(SharpStream target, long start, long end) throws IOException {
|
||||||
length = end - start;
|
source = target;
|
||||||
position = 0;
|
offset = start;
|
||||||
|
length = end - start;
|
||||||
if (length < 1) {
|
position = 0;
|
||||||
source.close();
|
|
||||||
throw new IOException("The chunk is empty or invalid");
|
if (length < 1) {
|
||||||
}
|
source.close();
|
||||||
if (source.length() < end) {
|
throw new IOException("The chunk is empty or invalid");
|
||||||
try {
|
}
|
||||||
throw new IOException(String.format("invalid file length. expected = %s found = %s", end, source.length()));
|
if (source.length() < end) {
|
||||||
} finally {
|
try {
|
||||||
source.close();
|
throw new IOException(String.format("invalid file length. expected = %s found = %s", end, source.length()));
|
||||||
}
|
} finally {
|
||||||
}
|
source.close();
|
||||||
|
}
|
||||||
source.seek(offset);
|
}
|
||||||
}
|
|
||||||
|
source.seek(offset);
|
||||||
/**
|
}
|
||||||
* Get absolute position on file
|
|
||||||
*
|
/**
|
||||||
* @return the position
|
* Get absolute position on file
|
||||||
*/
|
*
|
||||||
public long getFilePointer() {
|
* @return the position
|
||||||
return offset + position;
|
*/
|
||||||
}
|
public long getFilePointer() {
|
||||||
|
return offset + position;
|
||||||
@Override
|
}
|
||||||
public int read() throws IOException {
|
|
||||||
if ((position + 1) > length) {
|
@Override
|
||||||
return 0;
|
public int read() throws IOException {
|
||||||
}
|
if ((position + 1) > length) {
|
||||||
|
return 0;
|
||||||
int res = source.read();
|
}
|
||||||
if (res >= 0) {
|
|
||||||
position++;
|
int res = source.read();
|
||||||
}
|
if (res >= 0) {
|
||||||
|
position++;
|
||||||
return res;
|
}
|
||||||
}
|
|
||||||
|
return res;
|
||||||
@Override
|
}
|
||||||
public int read(byte b[]) throws IOException {
|
|
||||||
return read(b, 0, b.length);
|
@Override
|
||||||
}
|
public int read(byte b[]) throws IOException {
|
||||||
|
return read(b, 0, b.length);
|
||||||
@Override
|
}
|
||||||
public int read(byte b[], int off, int len) throws IOException {
|
|
||||||
if ((position + len) > length) {
|
@Override
|
||||||
len = (int) (length - position);
|
public int read(byte b[], int off, int len) throws IOException {
|
||||||
}
|
if ((position + len) > length) {
|
||||||
if (len == 0) {
|
len = (int) (length - position);
|
||||||
return 0;
|
}
|
||||||
}
|
if (len == 0) {
|
||||||
|
return 0;
|
||||||
int res = source.read(b, off, len);
|
}
|
||||||
position += res;
|
|
||||||
|
int res = source.read(b, off, len);
|
||||||
return res;
|
position += res;
|
||||||
}
|
|
||||||
|
return res;
|
||||||
@Override
|
}
|
||||||
public long skip(long pos) throws IOException {
|
|
||||||
pos = Math.min(pos + position, length);
|
@Override
|
||||||
|
public long skip(long pos) throws IOException {
|
||||||
if (pos == 0) {
|
pos = Math.min(pos + position, length);
|
||||||
return 0;
|
|
||||||
}
|
if (pos == 0) {
|
||||||
|
return 0;
|
||||||
source.seek(offset + pos);
|
}
|
||||||
|
|
||||||
long oldPos = position;
|
source.seek(offset + pos);
|
||||||
position = pos;
|
|
||||||
|
long oldPos = position;
|
||||||
return pos - oldPos;
|
position = pos;
|
||||||
}
|
|
||||||
|
return pos - oldPos;
|
||||||
@Override
|
}
|
||||||
public int available() {
|
|
||||||
return (int) (length - position);
|
@Override
|
||||||
}
|
public long available() {
|
||||||
|
return (int) (length - position);
|
||||||
@SuppressWarnings("EmptyCatchBlock")
|
}
|
||||||
@Override
|
|
||||||
public void dispose() {
|
@SuppressWarnings("EmptyCatchBlock")
|
||||||
try {
|
@Override
|
||||||
source.close();
|
public void close() {
|
||||||
} catch (IOException err) {
|
source.close();
|
||||||
} finally {
|
source = null;
|
||||||
source = null;
|
}
|
||||||
}
|
|
||||||
}
|
@Override
|
||||||
|
public boolean isClosed() {
|
||||||
@Override
|
return source == null;
|
||||||
public boolean isDisposed() {
|
}
|
||||||
return source == null;
|
|
||||||
}
|
@Override
|
||||||
|
public void rewind() throws IOException {
|
||||||
@Override
|
position = 0;
|
||||||
public void rewind() throws IOException {
|
source.seek(offset);
|
||||||
position = 0;
|
}
|
||||||
source.seek(offset);
|
|
||||||
}
|
@Override
|
||||||
|
public boolean canRewind() {
|
||||||
@Override
|
return true;
|
||||||
public boolean canRewind() {
|
}
|
||||||
return true;
|
|
||||||
}
|
@Override
|
||||||
|
public boolean canRead() {
|
||||||
@Override
|
return true;
|
||||||
public boolean canRead() {
|
}
|
||||||
return true;
|
|
||||||
}
|
@Override
|
||||||
|
public boolean canWrite() {
|
||||||
@Override
|
return false;
|
||||||
public boolean canWrite() {
|
}
|
||||||
return false;
|
|
||||||
}
|
@Override
|
||||||
|
public void write(byte value) {
|
||||||
@Override
|
}
|
||||||
public void write(byte value) {
|
|
||||||
}
|
@Override
|
||||||
|
public void write(byte[] buffer) {
|
||||||
@Override
|
}
|
||||||
public void write(byte[] buffer) {
|
|
||||||
}
|
@Override
|
||||||
|
public void write(byte[] buffer, int offset, int count) {
|
||||||
@Override
|
}
|
||||||
public void write(byte[] buffer, int offset, int count) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void flush() {
|
|
||||||
}
|
|
||||||
}
|
|
497
app/src/main/java/us/shandian/giga/io/CircularFileWriter.java
Normal file
@ -0,0 +1,497 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
public class CircularFileWriter extends SharpStream {
|
||||||
|
|
||||||
|
private final static int QUEUE_BUFFER_SIZE = 8 * 1024;// 8 KiB
|
||||||
|
private final static int COPY_BUFFER_SIZE = 128 * 1024; // 128 KiB
|
||||||
|
private final static int NOTIFY_BYTES_INTERVAL = 64 * 1024;// 64 KiB
|
||||||
|
private final static int THRESHOLD_AUX_LENGTH = 15 * 1024 * 1024;// 15 MiB
|
||||||
|
|
||||||
|
private OffsetChecker callback;
|
||||||
|
|
||||||
|
public ProgressReport onProgress;
|
||||||
|
public WriteErrorHandle onWriteError;
|
||||||
|
|
||||||
|
private long reportPosition;
|
||||||
|
private long maxLengthKnown = -1;
|
||||||
|
|
||||||
|
private BufferedFile out;
|
||||||
|
private BufferedFile aux;
|
||||||
|
|
||||||
|
public CircularFileWriter(SharpStream target, File temp, OffsetChecker checker) throws IOException {
|
||||||
|
if (checker == null) {
|
||||||
|
throw new NullPointerException("checker is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!temp.exists()) {
|
||||||
|
if (!temp.createNewFile()) {
|
||||||
|
throw new IOException("Cannot create a temporal file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aux = new BufferedFile(temp);
|
||||||
|
out = new BufferedFile(target);
|
||||||
|
|
||||||
|
callback = checker;
|
||||||
|
|
||||||
|
reportPosition = NOTIFY_BYTES_INTERVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void flushAuxiliar(long amount) throws IOException {
|
||||||
|
if (aux.length < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.flush();
|
||||||
|
aux.flush();
|
||||||
|
|
||||||
|
boolean underflow = aux.offset < aux.length || out.offset < out.length;
|
||||||
|
byte[] buffer = new byte[COPY_BUFFER_SIZE];
|
||||||
|
|
||||||
|
aux.target.seek(0);
|
||||||
|
out.target.seek(out.length);
|
||||||
|
|
||||||
|
long length = amount;
|
||||||
|
while (length > 0) {
|
||||||
|
int read = (int) Math.min(length, Integer.MAX_VALUE);
|
||||||
|
read = aux.target.read(buffer, 0, Math.min(read, buffer.length));
|
||||||
|
|
||||||
|
if (read < 1) {
|
||||||
|
amount -= length;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.writeProof(buffer, read);
|
||||||
|
length -= read;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (underflow) {
|
||||||
|
if (out.offset >= out.length) {
|
||||||
|
// calculate the aux underflow pointer
|
||||||
|
if (aux.offset < amount) {
|
||||||
|
out.offset += aux.offset;
|
||||||
|
aux.offset = 0;
|
||||||
|
out.target.seek(out.offset);
|
||||||
|
} else {
|
||||||
|
aux.offset -= amount;
|
||||||
|
out.offset = out.length + amount;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
aux.offset = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.offset += amount;
|
||||||
|
aux.offset -= amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.length += amount;
|
||||||
|
|
||||||
|
if (out.length > maxLengthKnown) {
|
||||||
|
maxLengthKnown = out.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amount < aux.length) {
|
||||||
|
// move the excess data to the beginning of the file
|
||||||
|
long readOffset = amount;
|
||||||
|
long writeOffset = 0;
|
||||||
|
|
||||||
|
aux.length -= amount;
|
||||||
|
length = aux.length;
|
||||||
|
while (length > 0) {
|
||||||
|
int read = (int) Math.min(length, Integer.MAX_VALUE);
|
||||||
|
read = aux.target.read(buffer, 0, Math.min(read, buffer.length));
|
||||||
|
|
||||||
|
aux.target.seek(writeOffset);
|
||||||
|
aux.writeProof(buffer, read);
|
||||||
|
|
||||||
|
writeOffset += read;
|
||||||
|
readOffset += read;
|
||||||
|
length -= read;
|
||||||
|
|
||||||
|
aux.target.seek(readOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
aux.target.setLength(aux.length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aux.length > THRESHOLD_AUX_LENGTH) {
|
||||||
|
aux.target.setLength(THRESHOLD_AUX_LENGTH);// or setLength(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
aux.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flush any buffer and close the output file. Use this method if the
|
||||||
|
* operation is successful
|
||||||
|
*
|
||||||
|
* @return the final length of the file
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
public long finalizeFile() throws IOException {
|
||||||
|
flushAuxiliar(aux.length);
|
||||||
|
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
// change file length (if required)
|
||||||
|
long length = Math.max(maxLengthKnown, out.length);
|
||||||
|
if (length != out.target.length()) {
|
||||||
|
out.target.setLength(length);
|
||||||
|
}
|
||||||
|
|
||||||
|
close();
|
||||||
|
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the file without flushing any buffer
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
if (out != null) {
|
||||||
|
out.close();
|
||||||
|
out = null;
|
||||||
|
}
|
||||||
|
if (aux != null) {
|
||||||
|
aux.close();
|
||||||
|
aux = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(byte b) throws IOException {
|
||||||
|
write(new byte[]{b}, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(byte b[]) throws IOException {
|
||||||
|
write(b, 0, b.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(byte b[], int off, int len) throws IOException {
|
||||||
|
if (len == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long available;
|
||||||
|
long offsetOut = out.getOffset();
|
||||||
|
long offsetAux = aux.getOffset();
|
||||||
|
long end = callback.check();
|
||||||
|
|
||||||
|
if (end == -1) {
|
||||||
|
available = Integer.MAX_VALUE;
|
||||||
|
} else if (end < offsetOut) {
|
||||||
|
throw new IOException("The reported offset is invalid: " + end + "<" + offsetOut);
|
||||||
|
} else {
|
||||||
|
available = end - offsetOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean usingAux = aux.length > 0 && offsetOut >= out.length;
|
||||||
|
boolean underflow = offsetAux < aux.length || offsetOut < out.length;
|
||||||
|
|
||||||
|
if (usingAux) {
|
||||||
|
// before continue calculate the final length of aux
|
||||||
|
long length = offsetAux + len;
|
||||||
|
if (underflow) {
|
||||||
|
if (aux.length > length) {
|
||||||
|
length = aux.length;// the length is not changed
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
length = aux.length + len;
|
||||||
|
}
|
||||||
|
|
||||||
|
aux.write(b, off, len);
|
||||||
|
|
||||||
|
if (length >= THRESHOLD_AUX_LENGTH && length <= available) {
|
||||||
|
flushAuxiliar(available);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (underflow) {
|
||||||
|
available = out.length - offsetOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
int length = Math.min(len, (int) available);
|
||||||
|
out.write(b, off, length);
|
||||||
|
|
||||||
|
len -= length;
|
||||||
|
off += length;
|
||||||
|
|
||||||
|
if (len > 0) {
|
||||||
|
aux.write(b, off, len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onProgress != null) {
|
||||||
|
long absoluteOffset = out.getOffset() + aux.getOffset();
|
||||||
|
if (absoluteOffset > reportPosition) {
|
||||||
|
reportPosition = absoluteOffset + NOTIFY_BYTES_INTERVAL;
|
||||||
|
onProgress.report(absoluteOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void flush() throws IOException {
|
||||||
|
aux.flush();
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
long total = out.length + aux.length;
|
||||||
|
if (total > maxLengthKnown) {
|
||||||
|
maxLengthKnown = total;// save the current file length in case the method {@code rewind()} is called
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long skip(long amount) throws IOException {
|
||||||
|
seek(out.getOffset() + aux.getOffset() + amount);
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void rewind() throws IOException {
|
||||||
|
if (onProgress != null) {
|
||||||
|
onProgress.report(-out.length - aux.length);// rollback the whole progress
|
||||||
|
}
|
||||||
|
|
||||||
|
seek(0);
|
||||||
|
|
||||||
|
reportPosition = NOTIFY_BYTES_INTERVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void seek(long offset) throws IOException {
|
||||||
|
long total = out.length + aux.length;
|
||||||
|
|
||||||
|
if (offset == total) {
|
||||||
|
// do not ignore the seek offset if a underflow exists
|
||||||
|
long relativeOffset = out.getOffset() + aux.getOffset();
|
||||||
|
if (relativeOffset == total) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// flush everything, avoid any underflow
|
||||||
|
flush();
|
||||||
|
|
||||||
|
if (offset < 0 || offset > total) {
|
||||||
|
throw new IOException("desired offset is outside of range=0-" + total + " offset=" + offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset > out.length) {
|
||||||
|
out.seek(out.length);
|
||||||
|
aux.seek(offset - out.length);
|
||||||
|
} else {
|
||||||
|
out.seek(offset);
|
||||||
|
aux.seek(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isClosed() {
|
||||||
|
return out == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canRewind() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canWrite() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canSeek() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// <editor-fold defaultstate="collapsed" desc="stub read methods">
|
||||||
|
@Override
|
||||||
|
public boolean canRead() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read() {
|
||||||
|
throw new UnsupportedOperationException("write-only");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] buffer
|
||||||
|
) {
|
||||||
|
throw new UnsupportedOperationException("write-only");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] buffer, int offset, int count
|
||||||
|
) {
|
||||||
|
throw new UnsupportedOperationException("write-only");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long available() {
|
||||||
|
throw new UnsupportedOperationException("write-only");
|
||||||
|
}
|
||||||
|
//</editor-fold>
|
||||||
|
|
||||||
|
public interface OffsetChecker {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the amount of available space ahead
|
||||||
|
*
|
||||||
|
* @return absolute offset in the file where no more data SHOULD NOT be
|
||||||
|
* written. If the value is -1 the whole file will be used
|
||||||
|
*/
|
||||||
|
long check();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ProgressReport {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report the size of the new file
|
||||||
|
*
|
||||||
|
* @param progress the new size
|
||||||
|
*/
|
||||||
|
void report(long progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface WriteErrorHandle {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to handle a I/O exception
|
||||||
|
*
|
||||||
|
* @param err the cause
|
||||||
|
* @return {@code true} to retry and continue, otherwise, {@code false}
|
||||||
|
* and throw the exception
|
||||||
|
*/
|
||||||
|
boolean handle(Exception err);
|
||||||
|
}
|
||||||
|
|
||||||
|
class BufferedFile {
|
||||||
|
|
||||||
|
protected final SharpStream target;
|
||||||
|
|
||||||
|
private long offset;
|
||||||
|
protected long length;
|
||||||
|
|
||||||
|
private byte[] queue = new byte[QUEUE_BUFFER_SIZE];
|
||||||
|
private int queueSize;
|
||||||
|
|
||||||
|
BufferedFile(File file) throws FileNotFoundException {
|
||||||
|
this.target = new FileStream(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
BufferedFile(SharpStream target) {
|
||||||
|
this.target = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected long getOffset() {
|
||||||
|
return offset + queueSize;// absolute offset in the file
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void close() {
|
||||||
|
queue = null;
|
||||||
|
target.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void write(byte b[], int off, int len) throws IOException {
|
||||||
|
while (len > 0) {
|
||||||
|
// if the queue is full, the method available() will flush the queue
|
||||||
|
int read = Math.min(available(), len);
|
||||||
|
|
||||||
|
// enqueue incoming buffer
|
||||||
|
System.arraycopy(b, off, queue, queueSize, read);
|
||||||
|
queueSize += read;
|
||||||
|
|
||||||
|
len -= read;
|
||||||
|
off += read;
|
||||||
|
}
|
||||||
|
|
||||||
|
long total = offset + queueSize;
|
||||||
|
if (total > length) {
|
||||||
|
length = total;// save length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void flush() throws IOException {
|
||||||
|
writeProof(queue, queueSize);
|
||||||
|
offset += queueSize;
|
||||||
|
queueSize = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void rewind() throws IOException {
|
||||||
|
offset = 0;
|
||||||
|
target.seek(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int available() throws IOException {
|
||||||
|
if (queueSize >= queue.length) {
|
||||||
|
flush();
|
||||||
|
return queue.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return queue.length - queueSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() throws IOException {
|
||||||
|
offset = 0;
|
||||||
|
length = 0;
|
||||||
|
target.seek(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void seek(long absoluteOffset) throws IOException {
|
||||||
|
if (absoluteOffset == offset) {
|
||||||
|
return;// nothing to do
|
||||||
|
}
|
||||||
|
offset = absoluteOffset;
|
||||||
|
target.seek(absoluteOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
void writeProof(byte[] buffer, int length) throws IOException {
|
||||||
|
if (onWriteError == null) {
|
||||||
|
target.write(buffer, 0, length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
target.write(buffer, 0, length);
|
||||||
|
return;
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (!onWriteError.handle(e)) {
|
||||||
|
throw e;// give up
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
String absLength;
|
||||||
|
|
||||||
|
try {
|
||||||
|
absLength = Long.toString(target.length());
|
||||||
|
} catch (IOException e) {
|
||||||
|
absLength = "[" + e.getLocalizedMessage() + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
return String.format(
|
||||||
|
"offset=%s length=%s queue=%s absLength=%s",
|
||||||
|
offset, length, queueSize, absLength
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,126 +1,131 @@
|
|||||||
package us.shandian.giga.postprocessing.io;
|
package us.shandian.giga.io;
|
||||||
|
|
||||||
import org.schabi.newpipe.streams.io.SharpStream;
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
import java.io.IOException;
|
import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
import java.io.RandomAccessFile;
|
|
||||||
import java.nio.channels.FileChannel;
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
/**
|
import java.io.IOException;
|
||||||
* @author kapodamy
|
import java.io.RandomAccessFile;
|
||||||
*/
|
|
||||||
public class FileStream extends SharpStream {
|
/**
|
||||||
|
* @author kapodamy
|
||||||
public enum Mode {
|
*/
|
||||||
Read,
|
public class FileStream extends SharpStream {
|
||||||
ReadWrite
|
|
||||||
}
|
public RandomAccessFile source;
|
||||||
|
|
||||||
public RandomAccessFile source;
|
public FileStream(@NonNull File target) throws FileNotFoundException {
|
||||||
private final Mode mode;
|
this.source = new RandomAccessFile(target, "rw");
|
||||||
|
}
|
||||||
public FileStream(String path, Mode mode) throws IOException {
|
|
||||||
String flags;
|
public FileStream(@NonNull String path) throws FileNotFoundException {
|
||||||
|
this.source = new RandomAccessFile(path, "rw");
|
||||||
if (mode == Mode.Read) {
|
}
|
||||||
flags = "r";
|
|
||||||
} else {
|
@Override
|
||||||
flags = "rw";
|
public int read() throws IOException {
|
||||||
}
|
return source.read();
|
||||||
|
}
|
||||||
this.mode = mode;
|
|
||||||
source = new RandomAccessFile(path, flags);
|
@Override
|
||||||
}
|
public int read(byte b[]) throws IOException {
|
||||||
|
return source.read(b);
|
||||||
@Override
|
}
|
||||||
public int read() throws IOException {
|
|
||||||
return source.read();
|
@Override
|
||||||
}
|
public int read(byte b[], int off, int len) throws IOException {
|
||||||
|
return source.read(b, off, len);
|
||||||
@Override
|
}
|
||||||
public int read(byte b[]) throws IOException {
|
|
||||||
return read(b, 0, b.length);
|
@Override
|
||||||
}
|
public long skip(long pos) throws IOException {
|
||||||
|
return source.skipBytes((int) pos);
|
||||||
@Override
|
}
|
||||||
public int read(byte b[], int off, int len) throws IOException {
|
|
||||||
return source.read(b, off, len);
|
@Override
|
||||||
}
|
public long available() {
|
||||||
|
try {
|
||||||
@Override
|
return source.length() - source.getFilePointer();
|
||||||
public long skip(long pos) throws IOException {
|
} catch (IOException e) {
|
||||||
FileChannel fc = source.getChannel();
|
return 0;
|
||||||
fc.position(fc.position() + pos);
|
}
|
||||||
return pos;
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
@Override
|
public void close() {
|
||||||
public int available() {
|
if (source == null) return;
|
||||||
try {
|
try {
|
||||||
return (int) (source.length() - source.getFilePointer());
|
source.close();
|
||||||
} catch (IOException ex) {
|
} catch (IOException err) {
|
||||||
return 0;
|
// nothing to do
|
||||||
}
|
}
|
||||||
}
|
source = null;
|
||||||
|
}
|
||||||
@SuppressWarnings("EmptyCatchBlock")
|
|
||||||
@Override
|
@Override
|
||||||
public void dispose() {
|
public boolean isClosed() {
|
||||||
try {
|
return source == null;
|
||||||
source.close();
|
}
|
||||||
} catch (IOException err) {
|
|
||||||
|
@Override
|
||||||
} finally {
|
public void rewind() throws IOException {
|
||||||
source = null;
|
source.seek(0);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
@Override
|
public boolean canRewind() {
|
||||||
public boolean isDisposed() {
|
return true;
|
||||||
return source == null;
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
@Override
|
public boolean canRead() {
|
||||||
public void rewind() throws IOException {
|
return true;
|
||||||
source.getChannel().position(0);
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
@Override
|
public boolean canWrite() {
|
||||||
public boolean canRewind() {
|
return true;
|
||||||
return true;
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
@Override
|
public boolean canSeek() {
|
||||||
public boolean canRead() {
|
return true;
|
||||||
return mode == Mode.Read || mode == Mode.ReadWrite;
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
@Override
|
public boolean canSetLength() {
|
||||||
public boolean canWrite() {
|
return true;
|
||||||
return mode == Mode.ReadWrite;
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
@Override
|
public void write(byte value) throws IOException {
|
||||||
public void write(byte value) throws IOException {
|
source.write(value);
|
||||||
source.write(value);
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
@Override
|
public void write(byte[] buffer) throws IOException {
|
||||||
public void write(byte[] buffer) throws IOException {
|
source.write(buffer);
|
||||||
source.write(buffer);
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
@Override
|
public void write(byte[] buffer, int offset, int count) throws IOException {
|
||||||
public void write(byte[] buffer, int offset, int count) throws IOException {
|
source.write(buffer, offset, count);
|
||||||
source.write(buffer, offset, count);
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
@Override
|
public void setLength(long length) throws IOException {
|
||||||
public void flush() {
|
source.setLength(length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setLength(long length) throws IOException {
|
public void seek(long offset) throws IOException {
|
||||||
source.setLength(length);
|
source.seek(offset);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
|
public long length() throws IOException {
|
||||||
|
return source.length();
|
||||||
|
}
|
||||||
|
}
|
145
app/src/main/java/us/shandian/giga/io/FileStreamSAF.java
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long length() throws IOException {
|
||||||
|
return channel.size();
|
||||||
|
}
|
||||||
|
}
|
@ -1,59 +1,61 @@
|
|||||||
/*
|
/*
|
||||||
* To change this license header, choose License Headers in Project Properties.
|
* To change this license header, choose License Headers in Project Properties.
|
||||||
* 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;
|
||||||
|
|
||||||
import org.schabi.newpipe.streams.io.SharpStream;
|
import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper for the classic {@link java.io.InputStream}
|
* Wrapper for the classic {@link java.io.InputStream}
|
||||||
* @author kapodamy
|
*
|
||||||
*/
|
* @author kapodamy
|
||||||
public class SharpInputStream extends InputStream {
|
*/
|
||||||
|
public class SharpInputStream extends InputStream {
|
||||||
private final SharpStream base;
|
|
||||||
|
private final SharpStream base;
|
||||||
public SharpInputStream(SharpStream base) throws IOException {
|
|
||||||
if (!base.canRead()) {
|
public SharpInputStream(SharpStream base) throws IOException {
|
||||||
throw new IOException("The provided stream is not readable");
|
if (!base.canRead()) {
|
||||||
}
|
throw new IOException("The provided stream is not readable");
|
||||||
this.base = base;
|
}
|
||||||
}
|
this.base = base;
|
||||||
|
}
|
||||||
@Override
|
|
||||||
public int read() throws IOException {
|
@Override
|
||||||
return base.read();
|
public int read() throws IOException {
|
||||||
}
|
return base.read();
|
||||||
|
}
|
||||||
@Override
|
|
||||||
public int read(@NonNull byte[] bytes) throws IOException {
|
@Override
|
||||||
return base.read(bytes);
|
public int read(@NonNull byte[] bytes) throws IOException {
|
||||||
}
|
return base.read(bytes);
|
||||||
|
}
|
||||||
@Override
|
|
||||||
public int read(@NonNull byte[] bytes, int i, int i1) throws IOException {
|
@Override
|
||||||
return base.read(bytes, i, i1);
|
public int read(@NonNull byte[] bytes, int i, int i1) throws IOException {
|
||||||
}
|
return base.read(bytes, i, i1);
|
||||||
|
}
|
||||||
@Override
|
|
||||||
public long skip(long l) throws IOException {
|
@Override
|
||||||
return base.skip(l);
|
public long skip(long l) throws IOException {
|
||||||
}
|
return base.skip(l);
|
||||||
|
}
|
||||||
@Override
|
|
||||||
public int available() {
|
@Override
|
||||||
return base.available();
|
public int available() {
|
||||||
}
|
long res = base.available();
|
||||||
|
return res > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) res;
|
||||||
@Override
|
}
|
||||||
public void close() {
|
|
||||||
base.dispose();
|
@Override
|
||||||
}
|
public void close() {
|
||||||
}
|
base.close();
|
||||||
|
}
|
||||||
|
}
|
289
app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
package us.shandian.giga.io;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.provider.DocumentsContract;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v4.provider.DocumentFile;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import static android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME;
|
||||||
|
import static android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID;
|
||||||
|
|
||||||
|
|
||||||
|
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 Context context;
|
||||||
|
|
||||||
|
private String tag;
|
||||||
|
|
||||||
|
public StoredDirectoryHelper(@NonNull Context context, @NonNull Uri path, String tag) throws IOException {
|
||||||
|
this.tag = tag;
|
||||||
|
|
||||||
|
if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(path.getScheme())) {
|
||||||
|
this.ioTree = new File(URI.create(path.toString()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.context = context;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.context.getContentResolver().takePersistableUriPermission(path, PERMISSION_FLAGS);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
throw new IOException("Storage Access Framework with Directory API is not available");
|
||||||
|
|
||||||
|
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 URI location, String tag) {
|
||||||
|
ioTree = new File(location);
|
||||||
|
this.tag = tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StoredFileHelper createFile(String filename, String mime) {
|
||||||
|
return createFile(filename, mime, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StoredFileHelper createUniqueFile(String name, String mime) {
|
||||||
|
ArrayList<String> matches = new ArrayList<>();
|
||||||
|
String[] filename = splitFilename(name);
|
||||||
|
String lcFilename = filename[0].toLowerCase();
|
||||||
|
|
||||||
|
if (docTree == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
for (File file : ioTree.listFiles())
|
||||||
|
addIfStartWith(matches, lcFilename, file.getName());
|
||||||
|
} else {
|
||||||
|
// warning: SAF file listing is very slow
|
||||||
|
Uri docTreeChildren = DocumentsContract.buildChildDocumentsUriUsingTree(
|
||||||
|
docTree.getUri(), DocumentsContract.getDocumentId(docTree.getUri())
|
||||||
|
);
|
||||||
|
|
||||||
|
String[] projection = new String[]{COLUMN_DISPLAY_NAME};
|
||||||
|
String selection = "(LOWER(" + COLUMN_DISPLAY_NAME + ") LIKE ?%";
|
||||||
|
ContentResolver cr = context.getContentResolver();
|
||||||
|
|
||||||
|
try (Cursor cursor = cr.query(docTreeChildren, projection, selection, new String[]{lcFilename}, null)) {
|
||||||
|
if (cursor != null) {
|
||||||
|
while (cursor.moveToNext())
|
||||||
|
addIfStartWith(matches, lcFilename, cursor.getString(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches.size() < 1) {
|
||||||
|
return createFile(name, mime, true);
|
||||||
|
} else {
|
||||||
|
// check if the filename is in use
|
||||||
|
String lcName = name.toLowerCase();
|
||||||
|
for (String testName : matches) {
|
||||||
|
if (testName.equals(lcName)) {
|
||||||
|
lcName = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if not in use
|
||||||
|
if (lcName != null) return createFile(name, mime, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Collections.sort(matches, String::compareTo);
|
||||||
|
|
||||||
|
for (int i = 1; i < 1000; i++) {
|
||||||
|
if (Collections.binarySearch(matches, makeFileName(lcFilename, i, filename[1])) < 0)
|
||||||
|
return createFile(makeFileName(filename[0], i, filename[1]), mime, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return createFile(String.valueOf(System.currentTimeMillis()).concat(filename[1]), mime, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private StoredFileHelper createFile(String filename, String mime, boolean safe) {
|
||||||
|
StoredFileHelper storage;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (docTree == null)
|
||||||
|
storage = new StoredFileHelper(ioTree, filename, mime);
|
||||||
|
else
|
||||||
|
storage = new StoredFileHelper(context, docTree, filename, mime, safe);
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
storage.tag = tag;
|
||||||
|
|
||||||
|
return storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Uri getUri() {
|
||||||
|
return docTree == null ? Uri.fromFile(ioTree) : docTree.getUri();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean exists() {
|
||||||
|
return docTree == null ? ioTree.exists() : docTree.exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() {
|
||||||
|
return docTree == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only using Java I/O. Creates the directory named by this abstract pathname, including any
|
||||||
|
* necessary but nonexistent parent directories. Note that if this
|
||||||
|
* operation fails it may have succeeded in creating some of the necessary
|
||||||
|
* parent directories.
|
||||||
|
*
|
||||||
|
* @return <code>true</code> if and only if the directory was created,
|
||||||
|
* along with all necessary parent directories or already exists; <code>false</code>
|
||||||
|
* otherwise
|
||||||
|
*/
|
||||||
|
public boolean mkdirs() {
|
||||||
|
if (docTree == null) {
|
||||||
|
return ioTree.exists() || ioTree.mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (docTree.exists()) return true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
DocumentFile parent;
|
||||||
|
String child = docTree.getName();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
parent = docTree.getParentFile();
|
||||||
|
if (parent == null || child == null) break;
|
||||||
|
if (parent.exists()) return true;
|
||||||
|
|
||||||
|
parent.createDirectory(child);
|
||||||
|
|
||||||
|
child = parent.getName();// for the next iteration
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// no more parent directories or unsupported by the storage provider
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTag() {
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Uri findFile(String filename) {
|
||||||
|
if (docTree == null) {
|
||||||
|
File res = new File(ioTree, filename);
|
||||||
|
return res.exists() ? Uri.fromFile(res) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentFile res = findFileSAFHelper(context, docTree, filename);
|
||||||
|
return res == null ? null : res.getUri();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canWrite() {
|
||||||
|
return docTree == null ? ioTree.canWrite() : docTree.canWrite();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return docTree == null ? Uri.fromFile(ioTree).toString() : docTree.getUri().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
////////////////////
|
||||||
|
// Utils
|
||||||
|
///////////////////
|
||||||
|
|
||||||
|
private static void addIfStartWith(ArrayList<String> list, @NonNull String base, String str) {
|
||||||
|
if (str == null || str.isEmpty()) return;
|
||||||
|
str = str.toLowerCase();
|
||||||
|
if (str.startsWith(base)) list.add(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String[] splitFilename(@NonNull String filename) {
|
||||||
|
int dotIndex = filename.lastIndexOf('.');
|
||||||
|
|
||||||
|
if (dotIndex < 0 || (dotIndex == filename.length() - 1))
|
||||||
|
return new String[]{filename, ""};
|
||||||
|
|
||||||
|
return new String[]{filename.substring(0, dotIndex), filename.substring(dotIndex)};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String makeFileName(String name, int idx, String ext) {
|
||||||
|
return name.concat(" (").concat(String.valueOf(idx)).concat(")").concat(ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fast (but not enough) file/directory finder under the storage access framework
|
||||||
|
*
|
||||||
|
* @param context The context
|
||||||
|
* @param tree Directory where search
|
||||||
|
* @param filename Target filename
|
||||||
|
* @return A {@link android.support.v4.provider.DocumentFile} contain the reference, otherwise, null
|
||||||
|
*/
|
||||||
|
static DocumentFile findFileSAFHelper(@Nullable Context context, DocumentFile tree, String filename) {
|
||||||
|
if (context == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
return tree.findFile(filename);// warning: this is very slow
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tree.canRead()) return null;// missing read permission
|
||||||
|
|
||||||
|
final int name = 0;
|
||||||
|
final int documentId = 1;
|
||||||
|
|
||||||
|
// LOWER() SQL function is not supported
|
||||||
|
String selection = COLUMN_DISPLAY_NAME + " = ?";
|
||||||
|
//String selection = COLUMN_DISPLAY_NAME + " LIKE ?%";
|
||||||
|
|
||||||
|
Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(
|
||||||
|
tree.getUri(), DocumentsContract.getDocumentId(tree.getUri())
|
||||||
|
);
|
||||||
|
String[] projection = {COLUMN_DISPLAY_NAME, COLUMN_DOCUMENT_ID};
|
||||||
|
ContentResolver contentResolver = context.getContentResolver();
|
||||||
|
|
||||||
|
filename = filename.toLowerCase();
|
||||||
|
|
||||||
|
try (Cursor cursor = contentResolver.query(childrenUri, projection, selection, new String[]{filename}, null)) {
|
||||||
|
if (cursor == null) return null;
|
||||||
|
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
if (cursor.isNull(name) || !cursor.getString(name).toLowerCase().startsWith(filename))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
return DocumentFile.fromSingleUri(
|
||||||
|
context, DocumentsContract.buildDocumentUriUsingTree(
|
||||||
|
tree.getUri(), cursor.getString(documentId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
381
app/src/main/java/us/shandian/giga/io/StoredFileHelper.java
Normal file
@ -0,0 +1,381 @@
|
|||||||
|
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.annotation.Nullable;
|
||||||
|
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 Context context;
|
||||||
|
|
||||||
|
protected String source;
|
||||||
|
private String sourceTree;
|
||||||
|
|
||||||
|
protected String tag;
|
||||||
|
|
||||||
|
private String srcName;
|
||||||
|
private String srcType;
|
||||||
|
|
||||||
|
public StoredFileHelper(@Nullable Uri parent, 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;
|
||||||
|
if (parent != null) this.sourceTree = parent.toString();
|
||||||
|
|
||||||
|
this.tag = tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
StoredFileHelper(@Nullable Context context, DocumentFile tree, String filename, String mime, boolean safe) throws IOException {
|
||||||
|
this.docTree = tree;
|
||||||
|
this.context = context;
|
||||||
|
|
||||||
|
DocumentFile res;
|
||||||
|
|
||||||
|
if (safe) {
|
||||||
|
// no conflicts (the filename is not in use)
|
||||||
|
res = this.docTree.createFile(mime, filename);
|
||||||
|
if (res == null) throw new IOException("Cannot create the file");
|
||||||
|
} else {
|
||||||
|
res = createSAF(context, mime, filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.docFile = res;
|
||||||
|
|
||||||
|
this.source = docFile.getUri().toString();
|
||||||
|
this.sourceTree = docTree.getUri().toString();
|
||||||
|
|
||||||
|
this.srcName = this.docFile.getName();
|
||||||
|
this.srcType = this.docFile.getType();
|
||||||
|
}
|
||||||
|
|
||||||
|
StoredFileHelper(File location, String filename, String mime) throws IOException {
|
||||||
|
this.ioFile = new File(location, filename);
|
||||||
|
|
||||||
|
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.sourceTree = Uri.fromFile(location).toString();
|
||||||
|
|
||||||
|
this.srcName = ioFile.getName();
|
||||||
|
this.srcType = mime;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||||
|
public StoredFileHelper(Context context, @Nullable Uri parent, @NonNull Uri path, String tag) throws IOException {
|
||||||
|
this.tag = tag;
|
||||||
|
this.source = path.toString();
|
||||||
|
|
||||||
|
if (path.getScheme() == null || path.getScheme().equalsIgnoreCase(ContentResolver.SCHEME_FILE)) {
|
||||||
|
this.ioFile = new File(URI.create(this.source));
|
||||||
|
} else {
|
||||||
|
DocumentFile file = DocumentFile.fromSingleUri(context, path);
|
||||||
|
|
||||||
|
if (file == null) throw new RuntimeException("SAF not available");
|
||||||
|
|
||||||
|
this.context = context;
|
||||||
|
|
||||||
|
if (file.getName() == null) {
|
||||||
|
this.source = null;
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
this.docFile = file;
|
||||||
|
takePermissionSAF();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent != null) {
|
||||||
|
if (!ContentResolver.SCHEME_FILE.equals(parent.getScheme()))
|
||||||
|
this.docTree = DocumentFile.fromTreeUri(context, parent);
|
||||||
|
|
||||||
|
this.sourceTree = parent.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.srcName = getName();
|
||||||
|
this.srcType = getType();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static StoredFileHelper deserialize(@NonNull StoredFileHelper storage, Context context) throws IOException {
|
||||||
|
Uri treeUri = storage.sourceTree == null ? null : Uri.parse(storage.sourceTree);
|
||||||
|
|
||||||
|
if (storage.isInvalid())
|
||||||
|
return new StoredFileHelper(treeUri, storage.srcName, storage.srcType, storage.tag);
|
||||||
|
|
||||||
|
StoredFileHelper instance = new StoredFileHelper(context, treeUri, Uri.parse(storage.source), storage.tag);
|
||||||
|
|
||||||
|
// under SAF, if the target document is deleted, conserve the filename and mime
|
||||||
|
if (instance.srcName == null) instance.srcName = storage.srcName;
|
||||||
|
if (instance.srcType == null) instance.srcType = storage.srcType;
|
||||||
|
|
||||||
|
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(context.getContentResolver(), 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 Uri getParentUri() {
|
||||||
|
invalid();
|
||||||
|
|
||||||
|
return sourceTree == null ? null : Uri.parse(sourceTree);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void truncate() throws IOException {
|
||||||
|
invalid();
|
||||||
|
|
||||||
|
try (SharpStream fs = getStream()) {
|
||||||
|
fs.setLength(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean delete() {
|
||||||
|
if (source == null) return true;
|
||||||
|
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;
|
||||||
|
context.getContentResolver().releasePersistableUriPermission(docFile.getUri(), flags);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
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 String getName() {
|
||||||
|
if (source == null)
|
||||||
|
return srcName;
|
||||||
|
else if (docFile == null)
|
||||||
|
return ioFile.getName();
|
||||||
|
|
||||||
|
String name = docFile.getName();
|
||||||
|
return name == null ? srcName : name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
if (source == null || docFile == null)
|
||||||
|
return srcType;
|
||||||
|
|
||||||
|
String type = docFile.getType();
|
||||||
|
return type == null ? srcType : type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTag() {
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean existsAsFile() {
|
||||||
|
if (source == null) return false;
|
||||||
|
|
||||||
|
boolean exists = docFile == null ? ioFile.exists() : docFile.exists();
|
||||||
|
boolean isFile = docFile == null ? ioFile.isFile() : docFile.isFile();// ¿docFile.isVirtual() means is no-physical?
|
||||||
|
|
||||||
|
return exists && isFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean create() {
|
||||||
|
invalid();
|
||||||
|
boolean result;
|
||||||
|
|
||||||
|
if (docFile == null) {
|
||||||
|
try {
|
||||||
|
result = ioFile.createNewFile();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (docTree == null) {
|
||||||
|
result = false;
|
||||||
|
} else {
|
||||||
|
if (!docTree.canRead() || !docTree.canWrite()) return false;
|
||||||
|
try {
|
||||||
|
docFile = createSAF(context, srcType, srcName);
|
||||||
|
if (docFile == null || docFile.getName() == null) return false;
|
||||||
|
result = true;
|
||||||
|
} catch (IOException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
source = (docFile == null ? Uri.fromFile(ioFile) : docFile.getUri()).toString();
|
||||||
|
srcName = getName();
|
||||||
|
srcType = getType();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void invalidate() {
|
||||||
|
if (source == null) return;
|
||||||
|
|
||||||
|
srcName = getName();
|
||||||
|
srcType = getType();
|
||||||
|
|
||||||
|
source = null;
|
||||||
|
|
||||||
|
docTree = null;
|
||||||
|
docFile = null;
|
||||||
|
ioFile = null;
|
||||||
|
context = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean equals(StoredFileHelper storage) {
|
||||||
|
if (this == storage) return true;
|
||||||
|
|
||||||
|
// note: do not compare tags, files can have the same parent folder
|
||||||
|
//if (stringMismatch(this.tag, storage.tag)) return false;
|
||||||
|
|
||||||
|
if (stringMismatch(getLowerCase(this.sourceTree), getLowerCase(this.sourceTree)))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (this.isInvalid() || storage.isInvalid()) {
|
||||||
|
return this.srcName.equalsIgnoreCase(storage.srcName) && this.srcType.equalsIgnoreCase(storage.srcType);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void invalid() {
|
||||||
|
if (source == null)
|
||||||
|
throw new IllegalStateException("In invalid state");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void takePermissionSAF() throws IOException {
|
||||||
|
try {
|
||||||
|
context.getContentResolver().takePersistableUriPermission(docFile.getUri(), StoredDirectoryHelper.PERMISSION_FLAGS);
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (docFile.getName() == null) throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DocumentFile createSAF(@Nullable Context context, String mime, String filename) throws IOException {
|
||||||
|
DocumentFile res = StoredDirectoryHelper.findFileSAFHelper(context, docTree, 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(srcType == null ? DEFAULT_MIME : mime, filename);
|
||||||
|
if (res == null) throw new IOException("Cannot create the file");
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getLowerCase(String str) {
|
||||||
|
return str == null ? null : str.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean stringMismatch(String str1, String str2) {
|
||||||
|
if (str1 == null && str2 == null) return false;
|
||||||
|
if ((str1 == null) != (str2 == null)) return true;
|
||||||
|
|
||||||
|
return !str1.equals(str2);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package us.shandian.giga.postprocessing;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.streams.Mp4DashReader;
|
||||||
|
import org.schabi.newpipe.streams.Mp4FromDashWriter;
|
||||||
|
import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
class M4aNoDash extends Postprocessing {
|
||||||
|
|
||||||
|
M4aNoDash() {
|
||||||
|
super(false, true, ALGORITHM_M4A_NO_DASH);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
boolean test(SharpStream... sources) throws IOException {
|
||||||
|
// check if the mp4 file is DASH (youtube)
|
||||||
|
|
||||||
|
Mp4DashReader reader = new Mp4DashReader(sources[0]);
|
||||||
|
reader.parse();
|
||||||
|
|
||||||
|
switch (reader.getBrands()[0]) {
|
||||||
|
case 0x64617368:// DASH
|
||||||
|
case 0x69736F35:// ISO5
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
int process(SharpStream out, SharpStream... sources) throws IOException {
|
||||||
|
Mp4FromDashWriter muxer = new Mp4FromDashWriter(sources[0]);
|
||||||
|
muxer.setMainBrand(0x4D344120);// binary string "M4A "
|
||||||
|
muxer.parseSources();
|
||||||
|
muxer.selectTracks(0);
|
||||||
|
muxer.build(out);
|
||||||
|
|
||||||
|
return OK_RESULT;
|
||||||
|
}
|
||||||
|
}
|
@ -1,29 +1,27 @@
|
|||||||
package us.shandian.giga.postprocessing;
|
package us.shandian.giga.postprocessing;
|
||||||
|
|
||||||
import org.schabi.newpipe.streams.Mp4DashWriter;
|
import org.schabi.newpipe.streams.Mp4FromDashWriter;
|
||||||
import org.schabi.newpipe.streams.io.SharpStream;
|
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 Mp4DashMuxer extends Postprocessing {
|
Mp4FromDashMuxer() {
|
||||||
|
super(true, true, ALGORITHM_MP4_FROM_DASH_MUXER);
|
||||||
Mp4DashMuxer(DownloadMission mission) {
|
}
|
||||||
super(mission, 15360 * 1024/* 15 MiB */, true);
|
|
||||||
}
|
@Override
|
||||||
|
int process(SharpStream out, SharpStream... sources) throws IOException {
|
||||||
@Override
|
Mp4FromDashWriter muxer = new Mp4FromDashWriter(sources);
|
||||||
int process(SharpStream out, SharpStream... sources) throws IOException {
|
muxer.parseSources();
|
||||||
Mp4DashWriter muxer = new Mp4DashWriter(sources);
|
muxer.selectTracks(0, 0);
|
||||||
muxer.parseSources();
|
muxer.build(out);
|
||||||
muxer.selectTracks(0, 0);
|
|
||||||
muxer.build(out);
|
return OK_RESULT;
|
||||||
|
}
|
||||||
return OK_RESULT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
@ -1,136 +0,0 @@
|
|||||||
package us.shandian.giga.postprocessing;
|
|
||||||
|
|
||||||
import android.media.MediaCodec.BufferInfo;
|
|
||||||
import android.media.MediaExtractor;
|
|
||||||
import android.media.MediaMuxer;
|
|
||||||
import android.media.MediaMuxer.OutputFormat;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.streams.io.SharpStream;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
|
|
||||||
import us.shandian.giga.get.DownloadMission;
|
|
||||||
|
|
||||||
|
|
||||||
class Mp4Muxer extends Postprocessing {
|
|
||||||
private static final String TAG = "Mp4Muxer";
|
|
||||||
private static final int NOTIFY_BYTES_INTERVAL = 128 * 1024;// 128 KiB
|
|
||||||
|
|
||||||
Mp4Muxer(DownloadMission mission) {
|
|
||||||
super(mission, 0, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
int process(SharpStream out, SharpStream... sources) throws IOException {
|
|
||||||
File dlFile = mission.getDownloadedFile();
|
|
||||||
File tmpFile = new File(mission.location, mission.name.concat(".tmp"));
|
|
||||||
|
|
||||||
if (tmpFile.exists())
|
|
||||||
if (!tmpFile.delete()) return DownloadMission.ERROR_FILE_CREATION;
|
|
||||||
|
|
||||||
if (!tmpFile.createNewFile()) return DownloadMission.ERROR_FILE_CREATION;
|
|
||||||
|
|
||||||
FileInputStream source = null;
|
|
||||||
MediaMuxer muxer = null;
|
|
||||||
|
|
||||||
//noinspection TryFinallyCanBeTryWithResources
|
|
||||||
try {
|
|
||||||
source = new FileInputStream(dlFile);
|
|
||||||
MediaExtractor tracks[] = {
|
|
||||||
getMediaExtractor(source, mission.offsets[0], mission.offsets[1] - mission.offsets[0]),
|
|
||||||
getMediaExtractor(source, mission.offsets[1], mission.length - mission.offsets[1])
|
|
||||||
};
|
|
||||||
|
|
||||||
muxer = new MediaMuxer(tmpFile.getAbsolutePath(), OutputFormat.MUXER_OUTPUT_MPEG_4);
|
|
||||||
|
|
||||||
int tracksIndex[] = {
|
|
||||||
muxer.addTrack(tracks[0].getTrackFormat(0)),
|
|
||||||
muxer.addTrack(tracks[1].getTrackFormat(0))
|
|
||||||
};
|
|
||||||
|
|
||||||
ByteBuffer buffer = ByteBuffer.allocate(512 * 1024);// 512 KiB
|
|
||||||
BufferInfo info = new BufferInfo();
|
|
||||||
|
|
||||||
long written = 0;
|
|
||||||
long nextReport = NOTIFY_BYTES_INTERVAL;
|
|
||||||
|
|
||||||
muxer.start();
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
int done = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < tracks.length; i++) {
|
|
||||||
if (tracksIndex[i] < 0) continue;
|
|
||||||
|
|
||||||
info.set(0,
|
|
||||||
tracks[i].readSampleData(buffer, 0),
|
|
||||||
tracks[i].getSampleTime(),
|
|
||||||
tracks[i].getSampleFlags()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (info.size >= 0) {
|
|
||||||
muxer.writeSampleData(tracksIndex[i], buffer, info);
|
|
||||||
written += info.size;
|
|
||||||
done++;
|
|
||||||
}
|
|
||||||
if (!tracks[i].advance()) {
|
|
||||||
// EOF reached
|
|
||||||
tracks[i].release();
|
|
||||||
tracksIndex[i] = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (written > nextReport) {
|
|
||||||
nextReport = written + NOTIFY_BYTES_INTERVAL;
|
|
||||||
super.progressReport(written);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (done < 1) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// this part should not fail
|
|
||||||
if (!dlFile.delete()) return DownloadMission.ERROR_FILE_CREATION;
|
|
||||||
if (!tmpFile.renameTo(dlFile)) return DownloadMission.ERROR_FILE_CREATION;
|
|
||||||
|
|
||||||
return OK_RESULT;
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
if (muxer != null) {
|
|
||||||
muxer.stop();
|
|
||||||
muxer.release();
|
|
||||||
}
|
|
||||||
} catch (Exception err) {
|
|
||||||
if (DEBUG)
|
|
||||||
Log.e(TAG, "muxer stop/release failed", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (source != null) {
|
|
||||||
try {
|
|
||||||
source.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
// nothing to do
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the operation fails, delete the temporal file
|
|
||||||
if (tmpFile.exists()) {
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
tmpFile.delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private MediaExtractor getMediaExtractor(FileInputStream source, long offset, long length) throws IOException {
|
|
||||||
MediaExtractor extractor = new MediaExtractor();
|
|
||||||
extractor.setDataSource(source.getFD(), offset, length);
|
|
||||||
extractor.selectTrack(0);
|
|
||||||
|
|
||||||
return extractor;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,164 +1,256 @@
|
|||||||
package us.shandian.giga.postprocessing;
|
package us.shandian.giga.postprocessing;
|
||||||
|
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import org.schabi.newpipe.streams.io.SharpStream;
|
import android.util.Log;
|
||||||
|
|
||||||
import java.io.File;
|
import org.schabi.newpipe.streams.io.SharpStream;
|
||||||
import java.io.IOException;
|
|
||||||
|
import java.io.File;
|
||||||
import us.shandian.giga.get.DownloadMission;
|
import java.io.IOException;
|
||||||
import us.shandian.giga.postprocessing.io.ChunkFileInputStream;
|
import java.io.Serializable;
|
||||||
import us.shandian.giga.postprocessing.io.CircularFile;
|
|
||||||
import us.shandian.giga.service.DownloadManagerService;
|
import us.shandian.giga.get.DownloadMission;
|
||||||
|
import us.shandian.giga.io.ChunkFileInputStream;
|
||||||
public abstract class Postprocessing {
|
import us.shandian.giga.io.CircularFileWriter;
|
||||||
|
import us.shandian.giga.io.CircularFileWriter.OffsetChecker;
|
||||||
static final byte OK_RESULT = DownloadMission.ERROR_NOTHING;
|
import us.shandian.giga.service.DownloadManagerService;
|
||||||
|
|
||||||
public static final String ALGORITHM_TTML_CONVERTER = "ttml";
|
import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING;
|
||||||
public static final String ALGORITHM_MP4_DASH_MUXER = "mp4D";
|
import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD;
|
||||||
public static final String ALGORITHM_MP4_MUXER = "mp4";
|
import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION;
|
||||||
public static final String ALGORITHM_WEBM_MUXER = "webm";
|
|
||||||
|
public abstract class Postprocessing implements Serializable {
|
||||||
public static Postprocessing getAlgorithm(String algorithmName, DownloadMission mission) {
|
|
||||||
if (null == algorithmName) {
|
static transient final byte OK_RESULT = ERROR_NOTHING;
|
||||||
throw new NullPointerException("algorithmName");
|
|
||||||
} else switch (algorithmName) {
|
public transient static final String ALGORITHM_TTML_CONVERTER = "ttml";
|
||||||
case ALGORITHM_TTML_CONVERTER:
|
public transient static final String ALGORITHM_WEBM_MUXER = "webm";
|
||||||
return new TtmlConverter(mission);
|
public transient static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4";
|
||||||
case ALGORITHM_MP4_DASH_MUXER:
|
public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a";
|
||||||
return new Mp4DashMuxer(mission);
|
|
||||||
case ALGORITHM_MP4_MUXER:
|
public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args) {
|
||||||
return new Mp4Muxer(mission);
|
Postprocessing instance;
|
||||||
case ALGORITHM_WEBM_MUXER:
|
|
||||||
return new WebMMuxer(mission);
|
switch (algorithmName) {
|
||||||
/*case "example-algorithm":
|
case ALGORITHM_TTML_CONVERTER:
|
||||||
return new ExampleAlgorithm(mission);*/
|
instance = new TtmlConverter();
|
||||||
default:
|
break;
|
||||||
throw new RuntimeException("Unimplemented post-processing algorithm: " + algorithmName);
|
case ALGORITHM_WEBM_MUXER:
|
||||||
}
|
instance = new WebMMuxer();
|
||||||
}
|
break;
|
||||||
|
case ALGORITHM_MP4_FROM_DASH_MUXER:
|
||||||
/**
|
instance = new Mp4FromDashMuxer();
|
||||||
* Get a boolean value that indicate if the given algorithm work on the same
|
break;
|
||||||
* file
|
case ALGORITHM_M4A_NO_DASH:
|
||||||
*/
|
instance = new M4aNoDash();
|
||||||
public boolean worksOnSameFile;
|
break;
|
||||||
|
/*case "example-algorithm":
|
||||||
/**
|
instance = new ExampleAlgorithm();*/
|
||||||
* Get the recommended space to reserve for the given algorithm. The amount
|
default:
|
||||||
* is in bytes
|
throw new UnsupportedOperationException("Unimplemented post-processing algorithm: " + algorithmName);
|
||||||
*/
|
}
|
||||||
public int recommendedReserve;
|
|
||||||
|
instance.args = args;
|
||||||
/**
|
return instance;
|
||||||
* the download to post-process
|
}
|
||||||
*/
|
|
||||||
protected DownloadMission mission;
|
/**
|
||||||
|
* Get a boolean value that indicate if the given algorithm work on the same
|
||||||
Postprocessing(DownloadMission mission, int recommendedReserve, boolean worksOnSameFile) {
|
* file
|
||||||
this.mission = mission;
|
*/
|
||||||
this.recommendedReserve = recommendedReserve;
|
public final boolean worksOnSameFile;
|
||||||
this.worksOnSameFile = worksOnSameFile;
|
|
||||||
}
|
/**
|
||||||
|
* Indicates whether the selected algorithm needs space reserved at the beginning of the file
|
||||||
public void run() throws IOException {
|
*/
|
||||||
File file = mission.getDownloadedFile();
|
public final boolean reserveSpace;
|
||||||
CircularFile out = null;
|
|
||||||
int result;
|
/**
|
||||||
long finalLength = -1;
|
* Gets the given algorithm short name
|
||||||
|
*/
|
||||||
mission.done = 0;
|
private final String name;
|
||||||
mission.length = file.length();
|
|
||||||
|
|
||||||
if (worksOnSameFile) {
|
private String[] args;
|
||||||
ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length];
|
|
||||||
try {
|
protected transient DownloadMission mission;
|
||||||
int i = 0;
|
|
||||||
for (; i < sources.length - 1; i++) {
|
private File tempFile;
|
||||||
sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.offsets[i + 1], "rw");
|
|
||||||
}
|
Postprocessing(boolean reserveSpace, boolean worksOnSameFile, String algorithmName) {
|
||||||
sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.getDownloadedFile().length(), "rw");
|
this.reserveSpace = reserveSpace;
|
||||||
|
this.worksOnSameFile = worksOnSameFile;
|
||||||
int[] idx = {0};
|
this.name = algorithmName;// for debugging only
|
||||||
CircularFile.OffsetChecker checker = () -> {
|
}
|
||||||
while (idx[0] < sources.length) {
|
|
||||||
/*
|
public void setTemporalDir(@NonNull File directory) {
|
||||||
* WARNING: never use rewind() in any chunk after any writing (especially on first chunks)
|
long rnd = (int) (Math.random() * 100000f);
|
||||||
* or the CircularFile can lead to unexpected results
|
tempFile = new File(directory, rnd + "_" + System.nanoTime() + ".tmp");
|
||||||
*/
|
}
|
||||||
if (sources[idx[0]].isDisposed() || sources[idx[0]].available() < 1) {
|
|
||||||
idx[0]++;
|
public void cleanupTemporalDir() {
|
||||||
continue;// the selected source is not used anymore
|
if (tempFile != null && tempFile.exists()) {
|
||||||
}
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
tempFile.delete();
|
||||||
return sources[idx[0]].getFilePointer() - 1;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return -1;
|
|
||||||
};
|
public void run(DownloadMission target) throws IOException {
|
||||||
out = new CircularFile(file, 0, this::progressReport, checker);
|
this.mission = target;
|
||||||
|
|
||||||
result = process(out, sources);
|
CircularFileWriter out = null;
|
||||||
|
int result;
|
||||||
if (result == OK_RESULT)
|
long finalLength = -1;
|
||||||
finalLength = out.finalizeFile();
|
|
||||||
} finally {
|
mission.done = 0;
|
||||||
for (SharpStream source : sources) {
|
mission.length = mission.storage.length();
|
||||||
if (source != null && !source.isDisposed()) {
|
|
||||||
source.dispose();
|
if (worksOnSameFile) {
|
||||||
}
|
ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length];
|
||||||
}
|
try {
|
||||||
if (out != null) {
|
int i = 0;
|
||||||
out.dispose();
|
for (; i < sources.length - 1; i++) {
|
||||||
}
|
sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i], mission.offsets[i + 1]);
|
||||||
}
|
}
|
||||||
} else {
|
sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i]);
|
||||||
result = process(null);
|
|
||||||
}
|
if (test(sources)) {
|
||||||
|
for (SharpStream source : sources) source.rewind();
|
||||||
if (result == OK_RESULT) {
|
|
||||||
if (finalLength < 0) finalLength = file.length();
|
OffsetChecker checker = () -> {
|
||||||
mission.done = finalLength;
|
for (ChunkFileInputStream source : sources) {
|
||||||
mission.length = finalLength;
|
/*
|
||||||
} else {
|
* WARNING: never use rewind() in any chunk after any writing (especially on first chunks)
|
||||||
mission.errCode = DownloadMission.ERROR_UNKNOWN_EXCEPTION;
|
* or the CircularFileWriter can lead to unexpected results
|
||||||
mission.errObject = new RuntimeException("post-processing algorithm returned " + result);
|
*/
|
||||||
}
|
if (source.isClosed() || source.available() < 1) {
|
||||||
|
continue;// the selected source is not used anymore
|
||||||
if (result != OK_RESULT && worksOnSameFile) {
|
}
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
file.delete();
|
return source.getFilePointer() - 1;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return -1;
|
||||||
/**
|
};
|
||||||
* Abstract method to execute the pos-processing algorithm
|
|
||||||
*
|
out = new CircularFileWriter(mission.storage.getStream(), tempFile, checker);
|
||||||
* @param out output stream
|
out.onProgress = this::progressReport;
|
||||||
* @param sources files to be processed
|
|
||||||
* @return a error code, 0 means the operation was successful
|
out.onWriteError = (err) -> {
|
||||||
* @throws IOException if an I/O error occurs.
|
mission.psState = 3;
|
||||||
*/
|
mission.notifyError(ERROR_POSTPROCESSING_HOLD, err);
|
||||||
abstract int process(SharpStream out, SharpStream... sources) throws IOException;
|
|
||||||
|
try {
|
||||||
String getArgumentAt(int index, String defaultValue) {
|
synchronized (this) {
|
||||||
if (mission.postprocessingArgs == null || index >= mission.postprocessingArgs.length) {
|
while (mission.psState == 3)
|
||||||
return defaultValue;
|
wait();
|
||||||
}
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
return mission.postprocessingArgs[index];
|
// nothing to do
|
||||||
}
|
Log.e(this.getClass().getSimpleName(), "got InterruptedException");
|
||||||
|
}
|
||||||
void progressReport(long done) {
|
|
||||||
mission.done = done;
|
return mission.errCode == ERROR_NOTHING;
|
||||||
if (mission.length < mission.done) mission.length = mission.done;
|
};
|
||||||
|
|
||||||
Message m = new Message();
|
result = process(out, sources);
|
||||||
m.what = DownloadManagerService.MESSAGE_PROGRESS;
|
|
||||||
m.obj = mission;
|
if (result == OK_RESULT)
|
||||||
|
finalLength = out.finalizeFile();
|
||||||
mission.mHandler.sendMessage(m);
|
} else {
|
||||||
}
|
result = OK_RESULT;
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
for (SharpStream source : sources) {
|
||||||
|
if (source != null && !source.isClosed()) {
|
||||||
|
source.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (out != null) {
|
||||||
|
out.close();
|
||||||
|
}
|
||||||
|
if (tempFile != null) {
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
tempFile.delete();
|
||||||
|
tempFile = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = test() ? process(null) : OK_RESULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == OK_RESULT) {
|
||||||
|
if (finalLength != -1) {
|
||||||
|
mission.done = finalLength;
|
||||||
|
mission.length = finalLength;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mission.errCode = ERROR_UNKNOWN_EXCEPTION;
|
||||||
|
mission.errObject = new RuntimeException("post-processing algorithm returned " + result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result != OK_RESULT && worksOnSameFile) mission.storage.delete();
|
||||||
|
|
||||||
|
this.mission = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test if the post-processing algorithm can be skipped
|
||||||
|
*
|
||||||
|
* @param sources files to be processed
|
||||||
|
* @return {@code true} if the post-processing is required, otherwise, {@code false}
|
||||||
|
* @throws IOException if an I/O error occurs.
|
||||||
|
*/
|
||||||
|
boolean test(SharpStream... sources) throws IOException {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract method to execute the post-processing algorithm
|
||||||
|
*
|
||||||
|
* @param out output stream
|
||||||
|
* @param sources files to be processed
|
||||||
|
* @return a error code, 0 means the operation was successful
|
||||||
|
* @throws IOException if an I/O error occurs.
|
||||||
|
*/
|
||||||
|
abstract int process(SharpStream out, SharpStream... sources) throws IOException;
|
||||||
|
|
||||||
|
String getArgumentAt(int index, String defaultValue) {
|
||||||
|
if (args == null || index >= args.length) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return args[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
private void progressReport(long done) {
|
||||||
|
mission.done = done;
|
||||||
|
if (mission.length < mission.done) mission.length = mission.done;
|
||||||
|
|
||||||
|
Message m = new Message();
|
||||||
|
m.what = DownloadManagerService.MESSAGE_PROGRESS;
|
||||||
|
m.obj = mission;
|
||||||
|
|
||||||
|
mission.mHandler.sendMessage(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder str = new StringBuilder();
|
||||||
|
|
||||||
|
str.append("name=").append(name).append('[');
|
||||||
|
|
||||||
|
if (args != null) {
|
||||||
|
for (String arg : args) {
|
||||||
|
str.append(", ");
|
||||||
|
str.append(arg);
|
||||||
|
}
|
||||||
|
str.delete(0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return str.append(']').toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,75 +1,72 @@
|
|||||||
package us.shandian.giga.postprocessing;
|
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;
|
||||||
import java.text.ParseException;
|
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
|
||||||
|
*/
|
||||||
/**
|
class TtmlConverter extends Postprocessing {
|
||||||
* @author kapodamy
|
private static final String TAG = "TtmlConverter";
|
||||||
*/
|
|
||||||
class TtmlConverter extends Postprocessing {
|
TtmlConverter() {
|
||||||
private static final String TAG = "TtmlConverter";
|
// due how XmlPullParser works, the xml is fully loaded on the ram
|
||||||
|
super(false, true, ALGORITHM_TTML_CONVERTER);
|
||||||
TtmlConverter(DownloadMission mission) {
|
}
|
||||||
// due how XmlPullParser works, the xml is fully loaded on the ram
|
|
||||||
super(mission, 0, true);
|
@Override
|
||||||
}
|
int process(SharpStream out, SharpStream... sources) throws IOException {
|
||||||
|
// check if the subtitle is already in srt and copy, this should never happen
|
||||||
@Override
|
String format = getArgumentAt(0, null);
|
||||||
int process(SharpStream out, SharpStream... sources) throws IOException {
|
|
||||||
// check if the subtitle is already in srt and copy, this should never happen
|
if (format == null || format.equals("ttml")) {
|
||||||
String format = getArgumentAt(0, null);
|
SubtitleConverter ttmlDumper = new SubtitleConverter();
|
||||||
|
|
||||||
if (format == null || format.equals("ttml")) {
|
try {
|
||||||
SubtitleConverter ttmlDumper = new SubtitleConverter();
|
ttmlDumper.dumpTTML(
|
||||||
|
sources[0],
|
||||||
try {
|
out,
|
||||||
ttmlDumper.dumpTTML(
|
getArgumentAt(1, "true").equals("true"),
|
||||||
sources[0],
|
getArgumentAt(2, "true").equals("true")
|
||||||
out,
|
);
|
||||||
getArgumentAt(1, "true").equals("true"),
|
} catch (Exception err) {
|
||||||
getArgumentAt(2, "true").equals("true")
|
Log.e(TAG, "subtitle parse failed", err);
|
||||||
);
|
|
||||||
} catch (Exception err) {
|
if (err instanceof IOException) {
|
||||||
Log.e(TAG, "subtitle parse failed", err);
|
return 1;
|
||||||
|
} else if (err instanceof ParseException) {
|
||||||
if (err instanceof IOException) {
|
return 2;
|
||||||
return 1;
|
} else if (err instanceof SAXException) {
|
||||||
} else if (err instanceof ParseException) {
|
return 3;
|
||||||
return 2;
|
} else if (err instanceof ParserConfigurationException) {
|
||||||
} else if (err instanceof SAXException) {
|
return 4;
|
||||||
return 3;
|
} else if (err instanceof XPathExpressionException) {
|
||||||
} else if (err instanceof ParserConfigurationException) {
|
return 7;
|
||||||
return 4;
|
}
|
||||||
} else if (err instanceof XPathExpressionException) {
|
|
||||||
return 7;
|
return 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 8;
|
return OK_RESULT;
|
||||||
}
|
} else if (format.equals("srt")) {
|
||||||
|
byte[] buffer = new byte[8 * 1024];
|
||||||
return OK_RESULT;
|
int read;
|
||||||
} else if (format.equals("srt")) {
|
while ((read = sources[0].read(buffer)) > 0) {
|
||||||
byte[] buffer = new byte[8 * 1024];
|
out.write(buffer, 0, read);
|
||||||
int read;
|
}
|
||||||
while ((read = sources[0].read(buffer)) > 0) {
|
return OK_RESULT;
|
||||||
out.write(buffer, 0, read);
|
}
|
||||||
}
|
|
||||||
return OK_RESULT;
|
throw new UnsupportedOperationException("Can't convert this subtitle, unimplemented format: " + format);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new UnsupportedOperationException("Can't convert this subtitle, unimplemented format: " + format);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -1,42 +1,44 @@
|
|||||||
package us.shandian.giga.postprocessing;
|
package us.shandian.giga.postprocessing;
|
||||||
|
|
||||||
import org.schabi.newpipe.streams.WebMReader.TrackKind;
|
import org.schabi.newpipe.streams.WebMReader.TrackKind;
|
||||||
import org.schabi.newpipe.streams.WebMReader.WebMTrack;
|
import org.schabi.newpipe.streams.WebMReader.WebMTrack;
|
||||||
import org.schabi.newpipe.streams.WebMWriter;
|
import org.schabi.newpipe.streams.WebMWriter;
|
||||||
import org.schabi.newpipe.streams.io.SharpStream;
|
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() {
|
||||||
|
super(true, true, ALGORITHM_WEBM_MUXER);
|
||||||
WebMMuxer(DownloadMission mission) {
|
}
|
||||||
super(mission, 2048 * 1024/* 2 MiB */, true);
|
|
||||||
}
|
@Override
|
||||||
|
int process(SharpStream out, SharpStream... sources) throws IOException {
|
||||||
@Override
|
WebMWriter muxer = new WebMWriter(sources);
|
||||||
int process(SharpStream out, SharpStream... sources) throws IOException {
|
muxer.parseSources();
|
||||||
WebMWriter muxer = new WebMWriter(sources);
|
|
||||||
muxer.parseSources();
|
// youtube uses a webm with a fake video track that acts as a "cover image"
|
||||||
|
int[] indexes = new int[sources.length];
|
||||||
// youtube uses a webm with a fake video track that acts as a "cover image"
|
|
||||||
WebMTrack[] tracks = muxer.getTracksFromSource(1);
|
for (int i = 0; i < sources.length; i++) {
|
||||||
int audioTrackIndex = 0;
|
WebMTrack[] tracks = muxer.getTracksFromSource(i);
|
||||||
for (int i = 0; i < tracks.length; i++) {
|
for (int j = 0; j < tracks.length; j++) {
|
||||||
if (tracks[i].kind == TrackKind.Audio) {
|
if (tracks[j].kind == TrackKind.Audio) {
|
||||||
audioTrackIndex = i;
|
indexes[i] = j;
|
||||||
break;
|
i = sources.length;
|
||||||
}
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
muxer.selectTracks(0, audioTrackIndex);
|
}
|
||||||
muxer.build(out);
|
|
||||||
|
muxer.selectTracks(indexes);
|
||||||
return OK_RESULT;
|
muxer.build(out);
|
||||||
}
|
|
||||||
|
return OK_RESULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -1,375 +0,0 @@
|
|||||||
package us.shandian.giga.postprocessing.io;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.streams.io.SharpStream;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.RandomAccessFile;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
public class CircularFile extends SharpStream {
|
|
||||||
|
|
||||||
private final static int AUX_BUFFER_SIZE = 1024 * 1024;// 1 MiB
|
|
||||||
private final static int AUX_BUFFER_SIZE2 = 512 * 1024;// 512 KiB
|
|
||||||
private final static int NOTIFY_BYTES_INTERVAL = 64 * 1024;// 64 KiB
|
|
||||||
private final static int QUEUE_BUFFER_SIZE = 8 * 1024;// 8 KiB
|
|
||||||
private final static boolean IMMEDIATE_AUX_BUFFER_FLUSH = false;
|
|
||||||
|
|
||||||
private RandomAccessFile out;
|
|
||||||
private long position;
|
|
||||||
private long maxLengthKnown = -1;
|
|
||||||
|
|
||||||
private ArrayList<ManagedBuffer> auxiliaryBuffers;
|
|
||||||
private OffsetChecker callback;
|
|
||||||
private ManagedBuffer queue;
|
|
||||||
private long startOffset;
|
|
||||||
private ProgressReport onProgress;
|
|
||||||
private long reportPosition;
|
|
||||||
|
|
||||||
public CircularFile(File file, long offset, ProgressReport progressReport, OffsetChecker checker) throws IOException {
|
|
||||||
if (checker == null) {
|
|
||||||
throw new NullPointerException("checker is null");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
queue = new ManagedBuffer(QUEUE_BUFFER_SIZE);
|
|
||||||
out = new RandomAccessFile(file, "rw");
|
|
||||||
out.seek(offset);
|
|
||||||
position = offset;
|
|
||||||
} catch (IOException err) {
|
|
||||||
try {
|
|
||||||
if (out != null) {
|
|
||||||
out.close();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
// nothing to do
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
auxiliaryBuffers = new ArrayList<>(15);
|
|
||||||
callback = checker;
|
|
||||||
startOffset = offset;
|
|
||||||
reportPosition = offset;
|
|
||||||
onProgress = progressReport;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close the file without flushing any buffer
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void dispose() {
|
|
||||||
try {
|
|
||||||
auxiliaryBuffers = null;
|
|
||||||
if (out != null) {
|
|
||||||
out.close();
|
|
||||||
out = null;
|
|
||||||
}
|
|
||||||
} catch (IOException err) {
|
|
||||||
// nothing to do
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flush any buffer and close the output file. Use this method if the
|
|
||||||
* operation is successful
|
|
||||||
*
|
|
||||||
* @return the final length of the file
|
|
||||||
* @throws IOException if an I/O error occurs
|
|
||||||
*/
|
|
||||||
public long finalizeFile() throws IOException {
|
|
||||||
flushEverything();
|
|
||||||
|
|
||||||
if (maxLengthKnown > -1) {
|
|
||||||
position = maxLengthKnown;
|
|
||||||
}
|
|
||||||
if (position < out.length()) {
|
|
||||||
out.setLength(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose();
|
|
||||||
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void write(byte b) throws IOException {
|
|
||||||
write(new byte[]{b}, 0, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void write(byte b[]) throws IOException {
|
|
||||||
write(b, 0, b.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void write(byte b[], int off, int len) throws IOException {
|
|
||||||
if (len == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
long end = callback.check();
|
|
||||||
long available;
|
|
||||||
|
|
||||||
if (end == -1) {
|
|
||||||
available = Long.MAX_VALUE;
|
|
||||||
} else {
|
|
||||||
if (end < startOffset) {
|
|
||||||
throw new IOException("The reported offset is invalid. reported offset is " + String.valueOf(end));
|
|
||||||
}
|
|
||||||
available = end - position;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if possible flush one or more auxiliary buffer
|
|
||||||
if (auxiliaryBuffers.size() > 0) {
|
|
||||||
ManagedBuffer aux = auxiliaryBuffers.get(0);
|
|
||||||
|
|
||||||
// check if there is enough space to flush it completely
|
|
||||||
while (available >= (aux.size + queue.size)) {
|
|
||||||
available -= aux.size;
|
|
||||||
writeQueue(aux.buffer, 0, aux.size);
|
|
||||||
aux.dereference();
|
|
||||||
auxiliaryBuffers.remove(0);
|
|
||||||
|
|
||||||
if (auxiliaryBuffers.size() < 1) {
|
|
||||||
aux = null;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
aux = auxiliaryBuffers.get(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IMMEDIATE_AUX_BUFFER_FLUSH) {
|
|
||||||
// try partial flush to avoid allocate another auxiliary buffer
|
|
||||||
if (aux != null && aux.available() < len && available > queue.size) {
|
|
||||||
int size = Math.min(aux.size, (int) available - queue.size);
|
|
||||||
|
|
||||||
writeQueue(aux.buffer, 0, size);
|
|
||||||
aux.dereference(size);
|
|
||||||
|
|
||||||
available -= size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auxiliaryBuffers.size() < 1 && available > (len + queue.size)) {
|
|
||||||
writeQueue(b, off, len);
|
|
||||||
} else {
|
|
||||||
int i = auxiliaryBuffers.size() - 1;
|
|
||||||
while (len > 0) {
|
|
||||||
if (i < 0) {
|
|
||||||
// allocate a new auxiliary buffer
|
|
||||||
auxiliaryBuffers.add(new ManagedBuffer(AUX_BUFFER_SIZE));
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
ManagedBuffer aux = auxiliaryBuffers.get(i);
|
|
||||||
available = aux.available();
|
|
||||||
|
|
||||||
if (available < 1) {
|
|
||||||
// secondary auxiliary buffer
|
|
||||||
available = len;
|
|
||||||
aux = new ManagedBuffer(Math.max(len, AUX_BUFFER_SIZE2));
|
|
||||||
auxiliaryBuffers.add(aux);
|
|
||||||
i++;
|
|
||||||
} else {
|
|
||||||
available = Math.min(len, available);
|
|
||||||
}
|
|
||||||
|
|
||||||
aux.write(b, off, (int) available);
|
|
||||||
|
|
||||||
len -= available;
|
|
||||||
if (len > 0) off += available;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeOutside(byte buffer[], int offset, int length) throws IOException {
|
|
||||||
out.write(buffer, offset, length);
|
|
||||||
position += length;
|
|
||||||
|
|
||||||
if (onProgress != null && position > reportPosition) {
|
|
||||||
reportPosition = position + NOTIFY_BYTES_INTERVAL;
|
|
||||||
onProgress.report(position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeQueue(byte[] buffer, int offset, int length) throws IOException {
|
|
||||||
while (length > 0) {
|
|
||||||
if (queue.available() < length) {
|
|
||||||
flushQueue();
|
|
||||||
|
|
||||||
if (length >= queue.buffer.length) {
|
|
||||||
writeOutside(buffer, offset, length);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int size = Math.min(queue.available(), length);
|
|
||||||
queue.write(buffer, offset, size);
|
|
||||||
|
|
||||||
offset += size;
|
|
||||||
length -= size;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (queue.size >= queue.buffer.length) {
|
|
||||||
flushQueue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void flushQueue() throws IOException {
|
|
||||||
writeOutside(queue.buffer, 0, queue.size);
|
|
||||||
queue.size = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void flushEverything() throws IOException {
|
|
||||||
flushQueue();
|
|
||||||
|
|
||||||
if (auxiliaryBuffers.size() > 0) {
|
|
||||||
for (ManagedBuffer aux : auxiliaryBuffers) {
|
|
||||||
writeOutside(aux.buffer, 0, aux.size);
|
|
||||||
aux.dereference();
|
|
||||||
}
|
|
||||||
auxiliaryBuffers.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flush any buffer directly to the file. Warning: use this method ONLY if
|
|
||||||
* all read dependencies are disposed
|
|
||||||
*
|
|
||||||
* @throws IOException if the dependencies are not disposed
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void flush() throws IOException {
|
|
||||||
if (callback.check() != -1) {
|
|
||||||
throw new IOException("All read dependencies of this file must be disposed first");
|
|
||||||
}
|
|
||||||
flushEverything();
|
|
||||||
|
|
||||||
// Save the current file length in case the method {@code rewind()} is called
|
|
||||||
if (position > maxLengthKnown) {
|
|
||||||
maxLengthKnown = position;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void rewind() throws IOException {
|
|
||||||
flush();
|
|
||||||
out.seek(startOffset);
|
|
||||||
|
|
||||||
if (onProgress != null) {
|
|
||||||
onProgress.report(-position);
|
|
||||||
}
|
|
||||||
|
|
||||||
position = startOffset;
|
|
||||||
reportPosition = startOffset;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long skip(long amount) throws IOException {
|
|
||||||
flush();
|
|
||||||
position += amount;
|
|
||||||
|
|
||||||
out.seek(position);
|
|
||||||
|
|
||||||
return amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isDisposed() {
|
|
||||||
return out == null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean canRewind() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean canWrite() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
//<editor-fold defaultState="collapsed" desc="stub read methods">
|
|
||||||
@Override
|
|
||||||
public boolean canRead() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read() {
|
|
||||||
throw new UnsupportedOperationException("write-only");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read(byte[] buffer) {
|
|
||||||
throw new UnsupportedOperationException("write-only");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read(byte[] buffer, int offset, int count) {
|
|
||||||
throw new UnsupportedOperationException("write-only");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int available() {
|
|
||||||
throw new UnsupportedOperationException("write-only");
|
|
||||||
}
|
|
||||||
//</editor-fold>
|
|
||||||
|
|
||||||
public interface OffsetChecker {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks the amount of available space ahead
|
|
||||||
*
|
|
||||||
* @return absolute offset in the file where no more data SHOULD NOT be
|
|
||||||
* written. If the value is -1 the whole file will be used
|
|
||||||
*/
|
|
||||||
long check();
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface ProgressReport {
|
|
||||||
|
|
||||||
void report(long progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
class ManagedBuffer {
|
|
||||||
|
|
||||||
byte[] buffer;
|
|
||||||
int size;
|
|
||||||
|
|
||||||
ManagedBuffer(int length) {
|
|
||||||
buffer = new byte[length];
|
|
||||||
}
|
|
||||||
|
|
||||||
void dereference() {
|
|
||||||
buffer = null;
|
|
||||||
size = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void dereference(int amount) {
|
|
||||||
if (amount > size) {
|
|
||||||
throw new IndexOutOfBoundsException("Invalid dereference amount (" + amount + ">=" + size + ")");
|
|
||||||
}
|
|
||||||
size -= amount;
|
|
||||||
System.arraycopy(buffer, amount, buffer, 0, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected int available() {
|
|
||||||
return buffer.length - size;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void write(byte[] b, int off, int len) {
|
|
||||||
System.arraycopy(b, off, buffer, size, len);
|
|
||||||
size += len;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "holding: " + String.valueOf(size) + " length: " + String.valueOf(buffer.length) + " available: " + String.valueOf(available());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -13,14 +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.io.StoredDirectoryHelper;
|
||||||
|
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;
|
||||||
@ -28,13 +29,16 @@ import static org.schabi.newpipe.BuildConfig.DEBUG;
|
|||||||
public class DownloadManager {
|
public class DownloadManager {
|
||||||
private static final String TAG = DownloadManager.class.getSimpleName();
|
private static final String TAG = DownloadManager.class.getSimpleName();
|
||||||
|
|
||||||
enum NetworkState {Unavailable, WifiOperating, MobileOperating, OtherOperating}
|
enum NetworkState {Unavailable, Operating, MeteredOperating}
|
||||||
|
|
||||||
public final static int SPECIAL_NOTHING = 0;
|
public final static int SPECIAL_NOTHING = 0;
|
||||||
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;
|
||||||
@ -45,7 +49,12 @@ public class DownloadManager {
|
|||||||
private NetworkState mLastNetworkStatus = NetworkState.Unavailable;
|
private NetworkState mLastNetworkStatus = NetworkState.Unavailable;
|
||||||
|
|
||||||
int mPrefMaxRetry;
|
int mPrefMaxRetry;
|
||||||
boolean mPrefCrossNetwork;
|
boolean mPrefMeteredDownloads;
|
||||||
|
boolean mPrefQueueLimit;
|
||||||
|
private boolean mSelfMissionsControl;
|
||||||
|
|
||||||
|
StoredDirectoryHelper mMainStorageAudio;
|
||||||
|
StoredDirectoryHelper mMainStorageVideo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new instance
|
* Create a new instance
|
||||||
@ -53,13 +62,15 @@ public class DownloadManager {
|
|||||||
* @param context Context for the data source for finished downloads
|
* @param context Context for the data source for finished downloads
|
||||||
* @param handler Thread required for Messaging
|
* @param handler Thread required for Messaging
|
||||||
*/
|
*/
|
||||||
DownloadManager(@NonNull Context context, Handler handler) {
|
DownloadManager(@NonNull Context context, Handler handler, StoredDirectoryHelper storageVideo, StoredDirectoryHelper storageAudio) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
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;
|
||||||
|
mMainStorageAudio = storageAudio;
|
||||||
|
mMainStorageVideo = storageVideo;
|
||||||
mMissionsFinished = loadFinishedMissions();
|
mMissionsFinished = loadFinishedMissions();
|
||||||
mPendingMissionsDir = getPendingDir(context);
|
mPendingMissionsDir = getPendingDir(context);
|
||||||
|
|
||||||
@ -67,7 +78,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) {
|
||||||
@ -88,29 +99,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);
|
||||||
mDownloadDataSource.deleteMission(mission);
|
finishedMissions.remove(i);
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result.add(mission);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return finishedMissions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadPendingMissions() {
|
private void loadPendingMissions(Context ctx) {
|
||||||
File[] subs = mPendingMissionsDir.listFiles();
|
File[] subs = mPendingMissionsDir.listFiles();
|
||||||
|
|
||||||
if (subs == null) {
|
if (subs == null) {
|
||||||
@ -125,109 +131,76 @@ public class DownloadManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (File sub : subs) {
|
for (File sub : subs) {
|
||||||
if (sub.isFile()) {
|
if (!sub.isFile()) continue;
|
||||||
DownloadMission mis = Utility.readFromFile(sub);
|
|
||||||
|
|
||||||
if (mis == null) {
|
DownloadMission mis = Utility.readFromFile(sub);
|
||||||
//noinspection ResultOfMethodCallIgnored
|
if (mis == null || mis.isFinished()) {
|
||||||
sub.delete();
|
//noinspection ResultOfMethodCallIgnored
|
||||||
} else {
|
sub.delete();
|
||||||
if (mis.isFinished()) {
|
continue;
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
sub.delete();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
File dl = mis.getDownloadedFile();
|
|
||||||
boolean exists = dl.exists();
|
|
||||||
|
|
||||||
if (mis.isPsRunning()) {
|
|
||||||
if (mis.postprocessingThis) {
|
|
||||||
// Incomplete post-processing results in a corrupted download file
|
|
||||||
// because the selected algorithm works on the same file to save space.
|
|
||||||
if (exists && dl.isFile() && !dl.delete())
|
|
||||||
Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath());
|
|
||||||
|
|
||||||
exists = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
mis.postprocessingState = 0;
|
|
||||||
mis.errCode = DownloadMission.ERROR_POSTPROCESSING;
|
|
||||||
mis.errObject = new RuntimeException("stopped unexpectedly");
|
|
||||||
} else if (exists && !dl.isFile()) {
|
|
||||||
// probably a folder, this should never happens
|
|
||||||
if (!sub.delete()) {
|
|
||||||
Log.w(TAG, "Unable to delete serialized file: " + sub.getPath());
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!exists) {
|
|
||||||
// downloaded file deleted, reset mission state
|
|
||||||
DownloadMission m = new DownloadMission(mis.urls, mis.name, mis.location, mis.kind, mis.postprocessingName, mis.postprocessingArgs);
|
|
||||||
m.timestamp = mis.timestamp;
|
|
||||||
m.threadCount = mis.threadCount;
|
|
||||||
m.source = mis.source;
|
|
||||||
m.maxRetry = mis.maxRetry;
|
|
||||||
m.nearLength = mis.nearLength;
|
|
||||||
mis = m;
|
|
||||||
}
|
|
||||||
|
|
||||||
mis.running = false;
|
|
||||||
mis.recovered = exists;
|
|
||||||
mis.metadata = sub;
|
|
||||||
mis.mHandler = mHandler;
|
|
||||||
|
|
||||||
mMissionsPending.add(mis);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean exists;
|
||||||
|
try {
|
||||||
|
mis.storage = StoredFileHelper.deserialize(mis.storage, ctx);
|
||||||
|
exists = !mis.storage.isInvalid() && mis.storage.existsAsFile();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Log.e(TAG, "Failed to load the file source of " + mis.storage.toString(), ex);
|
||||||
|
mis.storage.invalidate();
|
||||||
|
exists = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mis.isPsRunning()) {
|
||||||
|
if (mis.psAlgorithm.worksOnSameFile) {
|
||||||
|
// Incomplete post-processing results in a corrupted download file
|
||||||
|
// because the selected algorithm works on the same file to save space.
|
||||||
|
// the file will be deleted if the storage API
|
||||||
|
// is Java IO (avoid showing the "Save as..." dialog)
|
||||||
|
if (exists && mis.storage.isDirect() && !mis.storage.delete())
|
||||||
|
Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath());
|
||||||
|
|
||||||
|
exists = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
mis.psState = 0;
|
||||||
|
mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED;
|
||||||
|
} else if (!exists) {
|
||||||
|
tryRecover(mis);
|
||||||
|
|
||||||
|
// the progress is lost, reset mission state
|
||||||
|
if (mis.isInitialized())
|
||||||
|
mis.resetState(true, true, DownloadMission.ERROR_PROGRESS_LOST);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mis.psAlgorithm != null) {
|
||||||
|
mis.psAlgorithm.cleanupTemporalDir();
|
||||||
|
mis.psAlgorithm.setTemporalDir(pickAvailableTemporalDir(ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
mis.recovered = exists;
|
||||||
|
mis.metadata = sub;
|
||||||
|
mis.maxRetry = mPrefMaxRetry;
|
||||||
|
mis.mHandler = mHandler;
|
||||||
|
|
||||||
|
mMissionsPending.add(mis);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mMissionsPending.size() > 1) {
|
if (mMissionsPending.size() > 1)
|
||||||
Collections.sort(mMissionsPending, (mission1, mission2) -> Long.compare(mission1.timestamp, mission2.timestamp));
|
Collections.sort(mMissionsPending, (mission1, mission2) -> Long.compare(mission1.timestamp, mission2.timestamp));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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) {
|
|
||||||
// generate unique filename (?)
|
|
||||||
try {
|
|
||||||
name = generateUniqueName(location, name);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "Unable to generate unique name", e);
|
|
||||||
name = System.currentTimeMillis() + name;
|
|
||||||
Log.i(TAG, "Using " + name);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// check for existing finished download
|
|
||||||
int index = getFinishedMissionIndex(location, name);
|
|
||||||
if (index >= 0) mDownloadDataSource.deleteMission(mMissionsFinished.remove(index));
|
|
||||||
}
|
|
||||||
|
|
||||||
DownloadMission mission = new DownloadMission(urls, name, location, kind, 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()) {
|
||||||
@ -242,14 +215,25 @@ public class DownloadManager {
|
|||||||
mission.timestamp = System.currentTimeMillis();
|
mission.timestamp = System.currentTimeMillis();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mSelfMissionsControl = true;
|
||||||
mMissionsPending.add(mission);
|
mMissionsPending.add(mission);
|
||||||
|
|
||||||
// Before starting, save the state 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 (canDownloadInCurrentNetwork() && (getRunningMissionsCount() < 1)) {
|
if (mission.storage == null) {
|
||||||
|
// noting to do here
|
||||||
|
mission.errCode = DownloadMission.ERROR_FILE_CREATION;
|
||||||
|
if (mission.errObject != null)
|
||||||
|
mission.errObject = new IOException("DownloadMission.storage == NULL");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean start = !mPrefQueueLimit || getRunningMissionsCount() < 1;
|
||||||
|
|
||||||
|
if (canDownloadInCurrentNetwork() && start) {
|
||||||
|
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
|
||||||
mission.start();
|
mission.start();
|
||||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_RUNNING);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -257,13 +241,14 @@ public class DownloadManager {
|
|||||||
|
|
||||||
public void resumeMission(DownloadMission mission) {
|
public void resumeMission(DownloadMission mission) {
|
||||||
if (!mission.running) {
|
if (!mission.running) {
|
||||||
|
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
|
||||||
mission.start();
|
mission.start();
|
||||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_RUNNING);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void pauseMission(DownloadMission mission) {
|
public void pauseMission(DownloadMission mission) {
|
||||||
if (mission.running) {
|
if (mission.running) {
|
||||||
|
mission.setEnqueued(false);
|
||||||
mission.pause();
|
mission.pause();
|
||||||
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
|
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
|
||||||
}
|
}
|
||||||
@ -275,7 +260,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);
|
||||||
@ -283,18 +268,54 @@ 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void tryRecover(DownloadMission mission) {
|
||||||
|
StoredDirectoryHelper mainStorage = getMainStorage(mission.storage.getTag());
|
||||||
|
|
||||||
|
if (!mission.storage.isInvalid() && mission.storage.create()) return;
|
||||||
|
|
||||||
|
// using javaIO cannot recreate the file
|
||||||
|
// using SAF in older devices (no tree available)
|
||||||
|
//
|
||||||
|
// force the user to pick again the save path
|
||||||
|
mission.storage.invalidate();
|
||||||
|
|
||||||
|
if (mainStorage == null) return;
|
||||||
|
|
||||||
|
// if the user has changed the save path before this download, the original save path will be lost
|
||||||
|
StoredFileHelper newStorage = mainStorage.createFile(mission.storage.getName(), mission.storage.getType());
|
||||||
|
|
||||||
|
if (newStorage != null) mission.storage = newStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -302,16 +323,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -319,12 +338,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -335,7 +354,7 @@ public class DownloadManager {
|
|||||||
int count = 0;
|
int count = 0;
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
for (DownloadMission mission : mMissionsPending) {
|
for (DownloadMission mission : mMissionsPending) {
|
||||||
if (mission.running && !mission.isFinished() && !mission.isPsFailed())
|
if (mission.running && !mission.isPsFailed() && !mission.isFinished())
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -343,62 +362,36 @@ public class DownloadManager {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
void pauseAllMissions() {
|
public void pauseAllMissions(boolean force) {
|
||||||
|
boolean flag = false;
|
||||||
|
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
for (DownloadMission mission : mMissionsPending) mission.pause();
|
for (DownloadMission mission : mMissionsPending) {
|
||||||
}
|
if (!mission.running || mission.isPsRunning() || mission.isFinished()) continue;
|
||||||
}
|
|
||||||
|
|
||||||
|
if (force) mission.threads = null;// avoid waiting for threads
|
||||||
|
|
||||||
/**
|
mission.pause();
|
||||||
* Splits the filename into name and extension
|
flag = true;
|
||||||
* <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;
|
|
||||||
|
if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startAllMissions() {
|
||||||
|
boolean flag = false;
|
||||||
|
|
||||||
|
synchronized (this) {
|
||||||
|
for (DownloadMission mission : mMissionsPending) {
|
||||||
|
if (mission.running || mission.isCorrupt()) continue;
|
||||||
|
|
||||||
|
flag = true;
|
||||||
|
mission.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -410,36 +403,41 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* runs another mission in queue if possible
|
* runs one or multiple missions in from queue if possible
|
||||||
*
|
*
|
||||||
* @return true if exits pending missions running or a mission was started, otherwise, false
|
* @return true if one or multiple missions are running, otherwise, false
|
||||||
*/
|
*/
|
||||||
boolean runAnotherMission() {
|
boolean runMissions() {
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
if (mMissionsPending.size() < 1) return false;
|
if (mMissionsPending.size() < 1) return false;
|
||||||
|
|
||||||
int i = getRunningMissionsCount();
|
|
||||||
if (i > 0) return true;
|
|
||||||
|
|
||||||
if (!canDownloadInCurrentNetwork()) return false;
|
if (!canDownloadInCurrentNetwork()) return false;
|
||||||
|
|
||||||
for (DownloadMission mission : mMissionsPending) {
|
if (mPrefQueueLimit) {
|
||||||
if (!mission.running && mission.errCode == DownloadMission.ERROR_NOTHING && mission.enqueued) {
|
for (DownloadMission mission : mMissionsPending)
|
||||||
resumeMission(mission);
|
if (!mission.isFinished() && mission.running) return true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
boolean flag = false;
|
||||||
|
for (DownloadMission mission : mMissionsPending) {
|
||||||
|
if (mission.running || !mission.enqueued || mission.isFinished() || mission.hasInvalidStorage())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
resumeMission(mission);
|
||||||
|
if (mPrefQueueLimit) return true;
|
||||||
|
flag = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return flag;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public MissionIterator getIterator() {
|
public MissionIterator getIterator() {
|
||||||
|
mSelfMissionsControl = true;
|
||||||
return new MissionIterator();
|
return new MissionIterator();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -449,7 +447,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();
|
||||||
}
|
}
|
||||||
@ -457,31 +455,43 @@ public class DownloadManager {
|
|||||||
|
|
||||||
private boolean canDownloadInCurrentNetwork() {
|
private boolean canDownloadInCurrentNetwork() {
|
||||||
if (mLastNetworkStatus == NetworkState.Unavailable) return false;
|
if (mLastNetworkStatus == NetworkState.Unavailable) return false;
|
||||||
return !(mPrefCrossNetwork && mLastNetworkStatus == NetworkState.MobileOperating);
|
return !(mPrefMeteredDownloads && mLastNetworkStatus == NetworkState.MeteredOperating);
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleConnectivityChange(NetworkState currentStatus) {
|
void handleConnectivityState(NetworkState currentStatus, boolean updateOnly) {
|
||||||
if (currentStatus == mLastNetworkStatus) return;
|
if (currentStatus == mLastNetworkStatus) return;
|
||||||
|
|
||||||
mLastNetworkStatus = currentStatus;
|
mLastNetworkStatus = currentStatus;
|
||||||
|
if (currentStatus == NetworkState.Unavailable) return;
|
||||||
|
|
||||||
if (currentStatus == NetworkState.Unavailable) {
|
if (!mSelfMissionsControl || updateOnly) {
|
||||||
return;
|
return;// don't touch anything without the user interaction
|
||||||
} else if (currentStatus != NetworkState.MobileOperating || !mPrefCrossNetwork) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean flag = false;
|
boolean isMetered = mPrefMeteredDownloads && mLastNetworkStatus == NetworkState.MeteredOperating;
|
||||||
|
|
||||||
|
int running = 0;
|
||||||
|
int paused = 0;
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
for (DownloadMission mission : mMissionsPending) {
|
for (DownloadMission mission : mMissionsPending) {
|
||||||
if (mission.running && !mission.isFinished() && !mission.isPsRunning()) {
|
if (mission.isCorrupt() || mission.isPsRunning()) continue;
|
||||||
flag = true;
|
|
||||||
|
if (mission.running && isMetered) {
|
||||||
|
paused++;
|
||||||
mission.pause();
|
mission.pause();
|
||||||
|
} else if (!mission.running && !isMetered && mission.enqueued) {
|
||||||
|
running++;
|
||||||
|
mission.start();
|
||||||
|
if (mPrefQueueLimit) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
|
if (running > 0) {
|
||||||
|
mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (paused > 0) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED);
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateMaximumAttempts() {
|
void updateMaximumAttempts() {
|
||||||
@ -506,21 +516,46 @@ public class DownloadManager {
|
|||||||
), Toast.LENGTH_LONG).show();
|
), Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
void checkForRunningMission(String location, String name, DownloadManagerService.DMChecker check) {
|
public MissionState checkForExistingMission(StoredFileHelper storage) {
|
||||||
boolean listed;
|
|
||||||
boolean finished = false;
|
|
||||||
|
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
DownloadMission mission = getPendingMission(location, name);
|
DownloadMission pending = getPendingMission(storage);
|
||||||
if (mission != null) {
|
|
||||||
listed = true;
|
if (pending == null) {
|
||||||
|
if (getFinishedMissionIndex(storage) >= 0) return MissionState.Finished;
|
||||||
} else {
|
} else {
|
||||||
listed = getFinishedMissionIndex(location, name) >= 0;
|
if (pending.isFinished()) {
|
||||||
finished = listed;
|
return MissionState.Finished;// this never should happen (race-condition)
|
||||||
|
} else {
|
||||||
|
return pending.running ? MissionState.PendingRunning : MissionState.Pending;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
check.callback(listed, finished);
|
return MissionState.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isDirectoryAvailable(File directory) {
|
||||||
|
return directory != null && directory.canWrite() && directory.exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
static File pickAvailableTemporalDir(@NonNull Context ctx) {
|
||||||
|
if (isDirectoryAvailable(ctx.getExternalFilesDir(null)))
|
||||||
|
return ctx.getExternalFilesDir(null);
|
||||||
|
else if (isDirectoryAvailable(ctx.getFilesDir()))
|
||||||
|
return ctx.getFilesDir();
|
||||||
|
|
||||||
|
// this never should happen
|
||||||
|
return ctx.getDir("tmp", Context.MODE_PRIVATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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]: " + tag);
|
||||||
|
|
||||||
|
return null;// this never should happen
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MissionIterator extends DiffUtil.Callback {
|
public class MissionIterator extends DiffUtil.Callback {
|
||||||
@ -592,39 +627,6 @@ public class DownloadManager {
|
|||||||
return SPECIAL_NOTHING;
|
return SPECIAL_NOTHING;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MissionItem getItemUnsafe(int position) {
|
|
||||||
synchronized (DownloadManager.this) {
|
|
||||||
int count = mMissionsPending.size();
|
|
||||||
int count2 = mMissionsFinished.size();
|
|
||||||
|
|
||||||
if (count > 0) {
|
|
||||||
position--;
|
|
||||||
if (position == -1)
|
|
||||||
return new MissionItem(SPECIAL_PENDING);
|
|
||||||
else if (position < count)
|
|
||||||
return new MissionItem(SPECIAL_NOTHING, mMissionsPending.get(position));
|
|
||||||
else if (position == count && count2 > 0)
|
|
||||||
return new MissionItem(SPECIAL_FINISHED);
|
|
||||||
else
|
|
||||||
position -= count;
|
|
||||||
} else {
|
|
||||||
if (count2 > 0 && position == 0) {
|
|
||||||
return new MissionItem(SPECIAL_FINISHED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
position--;
|
|
||||||
|
|
||||||
if (count2 < 1) {
|
|
||||||
throw new RuntimeException(
|
|
||||||
String.format("Out of range. pending_count=%s finished_count=%s position=%s", count, count2, position)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new MissionItem(SPECIAL_NOTHING, mMissionsFinished.get(position));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void start() {
|
public void start() {
|
||||||
current = getSpecialItems();
|
current = getSpecialItems();
|
||||||
@ -647,6 +649,32 @@ public class DownloadManager {
|
|||||||
return hasFinished;
|
return hasFinished;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if exists missions running and paused. Corrupted and hidden missions are not counted
|
||||||
|
*
|
||||||
|
* @return two-dimensional array contains the current missions state.
|
||||||
|
* 1° entry: true if has at least one mission running
|
||||||
|
* 2° entry: true if has at least one mission paused
|
||||||
|
*/
|
||||||
|
public boolean[] hasValidPendingMissions() {
|
||||||
|
boolean running = false;
|
||||||
|
boolean paused = false;
|
||||||
|
|
||||||
|
synchronized (DownloadManager.this) {
|
||||||
|
for (DownloadMission mission : mMissionsPending) {
|
||||||
|
if (hidden.contains(mission) || mission.isCorrupt())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (mission.running)
|
||||||
|
paused = true;
|
||||||
|
else
|
||||||
|
running = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new boolean[]{running, paused};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getOldListSize() {
|
public int getOldListSize() {
|
||||||
@ -665,7 +693,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,21 +1,22 @@
|
|||||||
package us.shandian.giga.service;
|
package us.shandian.giga.service;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
|
import android.app.AlertDialog;
|
||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
import android.app.NotificationManager;
|
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;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
|
import android.net.Network;
|
||||||
import android.net.NetworkInfo;
|
import android.net.NetworkInfo;
|
||||||
|
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;
|
||||||
@ -24,6 +25,9 @@ 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.Nullable;
|
||||||
|
import android.support.annotation.StringRes;
|
||||||
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;
|
||||||
@ -36,9 +40,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;
|
||||||
@ -48,7 +56,6 @@ public class DownloadManagerService extends Service {
|
|||||||
|
|
||||||
private static final String TAG = "DownloadManagerService";
|
private static final String TAG = "DownloadManagerService";
|
||||||
|
|
||||||
public static final int MESSAGE_RUNNING = 0;
|
|
||||||
public static final int MESSAGE_PAUSED = 1;
|
public static final int MESSAGE_PAUSED = 1;
|
||||||
public static final int MESSAGE_FINISHED = 2;
|
public static final int MESSAGE_FINISHED = 2;
|
||||||
public static final int MESSAGE_PROGRESS = 3;
|
public static final int MESSAGE_PROGRESS = 3;
|
||||||
@ -59,24 +66,25 @@ 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_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_PATH = "DownloadManagerService.extra.storagePath";
|
||||||
|
private static final String EXTRA_PARENT_PATH = "DownloadManagerService.extra.storageParentPath";
|
||||||
|
private static final String EXTRA_STORAGE_TAG = "DownloadManagerService.extra.storageTag";
|
||||||
|
|
||||||
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;
|
||||||
private boolean mForeground = false;
|
private boolean mForeground = false;
|
||||||
private NotificationManager notificationManager = null;
|
private NotificationManager mNotificationManager = null;
|
||||||
private boolean mDownloadNotificationEnable = true;
|
private boolean mDownloadNotificationEnable = true;
|
||||||
|
|
||||||
private int downloadDoneCount = 0;
|
private int downloadDoneCount = 0;
|
||||||
@ -85,7 +93,9 @@ public class DownloadManagerService extends Service {
|
|||||||
|
|
||||||
private final ArrayList<Handler> mEchoObservers = new ArrayList<>(1);
|
private final ArrayList<Handler> mEchoObservers = new ArrayList<>(1);
|
||||||
|
|
||||||
private BroadcastReceiver mNetworkStateListener;
|
private ConnectivityManager mConnectivityManager;
|
||||||
|
private BroadcastReceiver mNetworkStateListener = null;
|
||||||
|
private ConnectivityManager.NetworkCallback mNetworkStateListenerL = null;
|
||||||
|
|
||||||
private SharedPreferences mPrefs = null;
|
private SharedPreferences mPrefs = null;
|
||||||
private final SharedPreferences.OnSharedPreferenceChangeListener mPrefChangeListener = this::handlePreferenceChange;
|
private final SharedPreferences.OnSharedPreferenceChangeListener mPrefChangeListener = this::handlePreferenceChange;
|
||||||
@ -106,10 +116,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
|
||||||
@ -120,7 +130,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) {
|
||||||
@ -128,7 +138,9 @@ public class DownloadManagerService extends Service {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mManager = new DownloadManager(this, mHandler);
|
mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
|
|
||||||
|
mManager = new DownloadManager(this, mHandler, loadMainVideoStorage(), loadMainAudioStorage());
|
||||||
|
|
||||||
Intent openDownloadListIntent = new Intent(this, DownloadActivity.class)
|
Intent openDownloadListIntent = new Intent(this, DownloadActivity.class)
|
||||||
.setAction(Intent.ACTION_MAIN);
|
.setAction(Intent.ACTION_MAIN);
|
||||||
@ -147,54 +159,55 @@ public class DownloadManagerService extends Service {
|
|||||||
.setContentText(getString(R.string.msg_running_detail));
|
.setContentText(getString(R.string.msg_running_detail));
|
||||||
|
|
||||||
mNotification = builder.build();
|
mNotification = builder.build();
|
||||||
notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
|
||||||
|
|
||||||
mNetworkStateListener = new BroadcastReceiver() {
|
mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
@Override
|
mConnectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
if (intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false)) {
|
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
handleConnectivityChange(null);
|
mNetworkStateListenerL = new ConnectivityManager.NetworkCallback() {
|
||||||
return;
|
@Override
|
||||||
|
public void onAvailable(Network network) {
|
||||||
|
handleConnectivityState(false);
|
||||||
}
|
}
|
||||||
handleConnectivityChange(intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
registerReceiver(mNetworkStateListener, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
|
|
||||||
|
|
||||||
mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
|
@Override
|
||||||
|
public void onLost(Network network) {
|
||||||
|
handleConnectivityState(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mConnectivityManager.registerNetworkCallback(new NetworkRequest.Builder().build(), mNetworkStateListenerL);
|
||||||
|
} else {
|
||||||
|
mNetworkStateListener = new BroadcastReceiver() {
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
handleConnectivityState(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
registerReceiver(mNetworkStateListener, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
|
||||||
|
}
|
||||||
|
|
||||||
mPrefs.registerOnSharedPreferenceChangeListener(mPrefChangeListener);
|
mPrefs.registerOnSharedPreferenceChangeListener(mPrefChangeListener);
|
||||||
|
|
||||||
handlePreferenceChange(mPrefs, getString(R.string.downloads_cross_network));
|
handlePreferenceChange(mPrefs, getString(R.string.downloads_cross_network));
|
||||||
handlePreferenceChange(mPrefs, getString(R.string.downloads_maximum_retry));
|
handlePreferenceChange(mPrefs, getString(R.string.downloads_maximum_retry));
|
||||||
|
handlePreferenceChange(mPrefs, getString(R.string.downloads_queue_limit));
|
||||||
|
|
||||||
mLock = new LockManager(this);
|
mLock = new LockManager(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@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) {
|
||||||
if (intent == null) {
|
Log.d(TAG, intent == null ? "Restarting" : "Starting");
|
||||||
Log.d(TAG, "Restarting");
|
|
||||||
return START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
Log.d(TAG, "Starting");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (intent == null) return START_NOT_STICKY;
|
||||||
|
|
||||||
Log.i(TAG, "Got intent: " + intent);
|
Log.i(TAG, "Got intent: " + intent);
|
||||||
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);
|
|
||||||
|
|
||||||
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;
|
||||||
@ -221,32 +234,36 @@ public class DownloadManagerService extends Service {
|
|||||||
|
|
||||||
stopForeground(true);
|
stopForeground(true);
|
||||||
|
|
||||||
if (notificationManager != null && downloadDoneNotification != null) {
|
if (mNotificationManager != null && downloadDoneNotification != null) {
|
||||||
downloadDoneNotification.setDeleteIntent(null);// prevent NewPipe running when is killed, cleared from recent, etc
|
downloadDoneNotification.setDeleteIntent(null);// prevent NewPipe running when is killed, cleared from recent, etc
|
||||||
notificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build());
|
mNotificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
mManager.pauseAllMissions();
|
|
||||||
|
|
||||||
manageLock(false);
|
manageLock(false);
|
||||||
|
|
||||||
unregisterReceiver(mNetworkStateListener);
|
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
mConnectivityManager.unregisterNetworkCallback(mNetworkStateListenerL);
|
||||||
|
else
|
||||||
|
unregisterReceiver(mNetworkStateListener);
|
||||||
|
|
||||||
mPrefs.unregisterOnSharedPreferenceChangeListener(mPrefChangeListener);
|
mPrefs.unregisterOnSharedPreferenceChangeListener(mPrefChangeListener);
|
||||||
|
|
||||||
if (icDownloadDone != null) icDownloadDone.recycle();
|
if (icDownloadDone != null) icDownloadDone.recycle();
|
||||||
if (icDownloadFailed != null) icDownloadFailed.recycle();
|
if (icDownloadFailed != null) icDownloadFailed.recycle();
|
||||||
if (icLauncher != null) icLauncher.recycle();
|
if (icLauncher != null) icLauncher.recycle();
|
||||||
|
|
||||||
|
mManager.pauseAllMissions(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@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) {
|
||||||
@ -261,18 +278,19 @@ 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);
|
||||||
updateForegroundState(mManager.runAnotherMission());
|
handleConnectivityState(false);
|
||||||
|
updateForegroundState(mManager.runMissions());
|
||||||
break;
|
break;
|
||||||
case MESSAGE_RUNNING:
|
|
||||||
case MESSAGE_PROGRESS:
|
case MESSAGE_PROGRESS:
|
||||||
updateForegroundState(true);
|
updateForegroundState(true);
|
||||||
break;
|
break;
|
||||||
case MESSAGE_ERROR:
|
case MESSAGE_ERROR:
|
||||||
notifyFailedDownload(mission);
|
notifyFailedDownload(mission);
|
||||||
updateForegroundState(mManager.runAnotherMission());
|
handleConnectivityState(false);
|
||||||
|
updateForegroundState(mManager.runMissions());
|
||||||
break;
|
break;
|
||||||
case MESSAGE_PAUSED:
|
case MESSAGE_PAUSED:
|
||||||
updateForegroundState(mManager.getRunningMissionsCount() > 0);
|
updateForegroundState(mManager.getRunningMissionsCount() > 0);
|
||||||
@ -293,46 +311,46 @@ public class DownloadManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleConnectivityChange(NetworkInfo info) {
|
private void handleConnectivityState(boolean updateOnly) {
|
||||||
|
NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
|
||||||
NetworkState status;
|
NetworkState status;
|
||||||
|
|
||||||
if (info == null) {
|
if (info == null) {
|
||||||
status = NetworkState.Unavailable;
|
status = NetworkState.Unavailable;
|
||||||
Log.i(TAG, "actual connectivity status is unavailable");
|
Log.i(TAG, "Active network [connectivity is unavailable]");
|
||||||
} else if (!info.isAvailable() || !info.isConnected()) {
|
|
||||||
status = NetworkState.Unavailable;
|
|
||||||
Log.i(TAG, "actual connectivity status is not available and not connected");
|
|
||||||
} else {
|
} else {
|
||||||
int type = info.getType();
|
boolean connected = info.isConnected();
|
||||||
if (type == ConnectivityManager.TYPE_MOBILE || type == ConnectivityManager.TYPE_MOBILE_DUN) {
|
boolean metered = mConnectivityManager.isActiveNetworkMetered();
|
||||||
status = NetworkState.MobileOperating;
|
|
||||||
} else if (type == ConnectivityManager.TYPE_WIFI) {
|
if (connected)
|
||||||
status = NetworkState.WifiOperating;
|
status = metered ? NetworkState.MeteredOperating : NetworkState.Operating;
|
||||||
} else if (type == ConnectivityManager.TYPE_WIMAX ||
|
else
|
||||||
type == ConnectivityManager.TYPE_ETHERNET ||
|
|
||||||
type == ConnectivityManager.TYPE_BLUETOOTH) {
|
|
||||||
status = NetworkState.OtherOperating;
|
|
||||||
} else {
|
|
||||||
status = NetworkState.Unavailable;
|
status = NetworkState.Unavailable;
|
||||||
}
|
|
||||||
Log.i(TAG, "actual connectivity status is " + status.name());
|
Log.i(TAG, "Active network [connected=" + connected + " metered=" + metered + "] " + info.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mManager == null) return;// avoid race-conditions while the service is starting
|
if (mManager == null) return;// avoid race-conditions while the service is starting
|
||||||
mManager.handleConnectivityChange(status);
|
mManager.handleConnectivityState(status, updateOnly);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handlePreferenceChange(SharedPreferences prefs, String key) {
|
private void handlePreferenceChange(SharedPreferences prefs, @NonNull String key) {
|
||||||
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;
|
||||||
}
|
}
|
||||||
mManager.updateMaximumAttempts();
|
mManager.updateMaximumAttempts();
|
||||||
} else if (key.equals(getString(R.string.downloads_cross_network))) {
|
} else if (key.equals(getString(R.string.downloads_cross_network))) {
|
||||||
mManager.mPrefCrossNetwork = prefs.getBoolean(key, false);
|
mManager.mPrefMeteredDownloads = prefs.getBoolean(key, false);
|
||||||
|
} else if (key.equals(getString(R.string.downloads_queue_limit))) {
|
||||||
|
mManager.mPrefQueueLimit = prefs.getBoolean(key, true);
|
||||||
|
} else if (key.equals(getString(R.string.download_path_video_key))) {
|
||||||
|
mManager.mMainStorageVideo = loadMainVideoStorage();
|
||||||
|
} else if (key.equals(getString(R.string.download_path_audio_key))) {
|
||||||
|
mManager.mMainStorageAudio = loadMainAudioStorage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -350,46 +368,78 @@ 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_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_PARENT_PATH, storage.getParentUri());
|
||||||
|
intent.putExtra(EXTRA_PATH, storage.getUri());
|
||||||
|
intent.putExtra(EXTRA_STORAGE_TAG, storage.getTag());
|
||||||
|
|
||||||
context.startService(intent);
|
context.startService(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void checkForRunningMission(Context context, String location, String name, DMChecker check) {
|
private 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.bindService(intent, new ServiceConnection() {
|
Uri parentPath = intent.getParcelableExtra(EXTRA_PARENT_PATH);
|
||||||
@Override
|
int threads = intent.getIntExtra(EXTRA_THREADS, 1);
|
||||||
public void onServiceConnected(ComponentName cname, IBinder service) {
|
char kind = intent.getCharExtra(EXTRA_KIND, '?');
|
||||||
try {
|
String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME);
|
||||||
((DMBinder) service).getDownloadManager().checkForRunningMission(location, name, check);
|
String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS);
|
||||||
} catch (Exception err) {
|
String source = intent.getStringExtra(EXTRA_SOURCE);
|
||||||
Log.w(TAG, "checkForRunningMission() callback is defective", err);
|
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
|
||||||
}
|
String tag = intent.getStringExtra(EXTRA_STORAGE_TAG);
|
||||||
|
|
||||||
// TODO: find a efficient way to unbind the service. This destroy the service due idle, but is started again when the user start a download.
|
StoredFileHelper storage;
|
||||||
context.unbindService(this);
|
try {
|
||||||
}
|
storage = new StoredFileHelper(this, parentPath, path, tag);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);// this never should happen
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
Postprocessing ps;
|
||||||
public void onServiceDisconnected(ComponentName name) {
|
if (psName == null)
|
||||||
}
|
ps = null;
|
||||||
}, Context.BIND_AUTO_CREATE);
|
else
|
||||||
|
ps = Postprocessing.getAlgorithm(psName, psArgs);
|
||||||
|
|
||||||
|
final DownloadMission mission = new DownloadMission(urls, storage, kind, ps);
|
||||||
|
mission.threadCount = threads;
|
||||||
|
mission.source = source;
|
||||||
|
mission.nearLength = nearLength;
|
||||||
|
|
||||||
|
if (ps != null)
|
||||||
|
ps.setTemporalDir(DownloadManager.pickAvailableTemporalDir(this));
|
||||||
|
|
||||||
|
handleConnectivityState(true);// first check the actual network status
|
||||||
|
|
||||||
|
mManager.startMission(mission);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void notifyFinishedDownload(String name) {
|
public void notifyFinishedDownload(String name) {
|
||||||
if (!mDownloadNotificationEnable || notificationManager == null) {
|
if (!mDownloadNotificationEnable || mNotificationManager == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -428,7 +478,7 @@ public class DownloadManagerService extends Service {
|
|||||||
downloadDoneNotification.setContentText(downloadDoneList);
|
downloadDoneNotification.setContentText(downloadDoneList);
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build());
|
mNotificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build());
|
||||||
downloadDoneCount++;
|
downloadDoneCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -450,15 +500,15 @@ 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()));
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationManager.notify(id, downloadFailedNotification.build());
|
mNotificationManager.notify(id, downloadFailedNotification.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
private PendingIntent makePendingIntent(String action) {
|
private PendingIntent makePendingIntent(String action) {
|
||||||
@ -487,12 +537,66 @@ public class DownloadManagerService extends Service {
|
|||||||
mLockAcquired = acquire;
|
mLockAcquired = acquire;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrapper of DownloadManager
|
private StoredDirectoryHelper loadMainVideoStorage() {
|
||||||
public class DMBinder extends Binder {
|
return loadMainStorage(R.string.download_path_video_key, DownloadManager.TAG_VIDEO);
|
||||||
|
}
|
||||||
|
|
||||||
|
private StoredDirectoryHelper loadMainAudioStorage() {
|
||||||
|
return loadMainStorage(R.string.download_path_audio_key, DownloadManager.TAG_AUDIO);
|
||||||
|
}
|
||||||
|
|
||||||
|
private StoredDirectoryHelper loadMainStorage(@StringRes int prefKey, String tag) {
|
||||||
|
String path = mPrefs.getString(getString(prefKey), null);
|
||||||
|
|
||||||
|
if (path == null || path.isEmpty()) return null;
|
||||||
|
|
||||||
|
if (path.charAt(0) == File.separatorChar) {
|
||||||
|
Log.i(TAG, "Old save path style present: " + path);
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
path = Uri.fromFile(new File(path)).toString();
|
||||||
|
else
|
||||||
|
path = "";
|
||||||
|
|
||||||
|
mPrefs.edit().putString(getString(prefKey), "").apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new StoredDirectoryHelper(this, Uri.parse(path), tag);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Failed to load the storage of " + tag + " from " + path, e);
|
||||||
|
Toast.makeText(this, R.string.no_available_dir, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Wrappers for DownloadManager
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
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 boolean askForSavePath() {
|
||||||
|
return DownloadManagerService.this.mPrefs.getBoolean(
|
||||||
|
DownloadManagerService.this.getString(R.string.downloads_storage_ask),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public void addMissionEventListener(Handler handler) {
|
public void addMissionEventListener(Handler handler) {
|
||||||
manageObservers(handler, true);
|
manageObservers(handler, true);
|
||||||
}
|
}
|
||||||
@ -502,15 +606,15 @@ public class DownloadManagerService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void clearDownloadNotifications() {
|
public void clearDownloadNotifications() {
|
||||||
if (notificationManager == null) return;
|
if (mNotificationManager == null) return;
|
||||||
if (downloadDoneNotification != null) {
|
if (downloadDoneNotification != null) {
|
||||||
notificationManager.cancel(DOWNLOADS_NOTIFICATION_ID);
|
mNotificationManager.cancel(DOWNLOADS_NOTIFICATION_ID);
|
||||||
downloadDoneList.setLength(0);
|
downloadDoneList.setLength(0);
|
||||||
downloadDoneCount = 0;
|
downloadDoneCount = 0;
|
||||||
}
|
}
|
||||||
if (downloadFailedNotification != null) {
|
if (downloadFailedNotification != null) {
|
||||||
for (; downloadFailedNotificationID > DOWNLOADS_NOTIFICATION_ID; downloadFailedNotificationID--) {
|
for (; downloadFailedNotificationID > DOWNLOADS_NOTIFICATION_ID; downloadFailedNotificationID--) {
|
||||||
notificationManager.cancel(downloadFailedNotificationID);
|
mNotificationManager.cancel(downloadFailedNotificationID);
|
||||||
}
|
}
|
||||||
mFailedDownloads.clear();
|
mFailedDownloads.clear();
|
||||||
downloadFailedNotificationID++;
|
downloadFailedNotificationID++;
|
||||||
@ -523,8 +627,4 @@ public class DownloadManagerService extends Service {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface DMChecker {
|
|
||||||
void callback(boolean listed, boolean finished);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
package us.shandian.giga.service;
|
||||||
|
|
||||||
|
public enum MissionState {
|
||||||
|
None, Pending, PendingRunning, Finished
|
||||||
|
}
|
@ -8,21 +8,22 @@ 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;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.annotation.StringRes;
|
||||||
import android.support.v4.content.FileProvider;
|
import android.support.v4.content.FileProvider;
|
||||||
import android.support.v4.view.ViewCompat;
|
import android.support.v4.view.ViewCompat;
|
||||||
import android.support.v7.app.AlertDialog;
|
import android.support.v7.app.AlertDialog;
|
||||||
import android.support.v7.util.DiffUtil;
|
import android.support.v7.util.DiffUtil;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.support.v7.widget.RecyclerView.ViewHolder;
|
|
||||||
import android.support.v7.widget.RecyclerView.Adapter;
|
import android.support.v7.widget.RecyclerView.Adapter;
|
||||||
|
import android.support.v7.widget.RecyclerView.ViewHolder;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
|
import android.view.HapticFeedbackConstants;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
@ -36,14 +37,20 @@ import android.widget.Toast;
|
|||||||
|
|
||||||
import org.schabi.newpipe.BuildConfig;
|
import org.schabi.newpipe.BuildConfig;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.report.ErrorActivity;
|
||||||
|
import org.schabi.newpipe.report.UserAction;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.net.URI;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
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.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;
|
||||||
@ -57,11 +64,16 @@ import static us.shandian.giga.get.DownloadMission.ERROR_CONNECT_HOST;
|
|||||||
import static us.shandian.giga.get.DownloadMission.ERROR_FILE_CREATION;
|
import static us.shandian.giga.get.DownloadMission.ERROR_FILE_CREATION;
|
||||||
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_NO_CONTENT;
|
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_NO_CONTENT;
|
||||||
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_UNSUPPORTED_RANGE;
|
import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_UNSUPPORTED_RANGE;
|
||||||
|
import static us.shandian.giga.get.DownloadMission.ERROR_INSUFFICIENT_STORAGE;
|
||||||
import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING;
|
import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING;
|
||||||
import static us.shandian.giga.get.DownloadMission.ERROR_PATH_CREATION;
|
import static us.shandian.giga.get.DownloadMission.ERROR_PATH_CREATION;
|
||||||
import static us.shandian.giga.get.DownloadMission.ERROR_PERMISSION_DENIED;
|
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_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_TIMEOUT;
|
||||||
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;
|
||||||
|
|
||||||
@ -69,6 +81,7 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||||||
private static final SparseArray<String> ALGORITHMS = new SparseArray<>();
|
private static final SparseArray<String> ALGORITHMS = new SparseArray<>();
|
||||||
private static final String TAG = "MissionAdapter";
|
private static final String TAG = "MissionAdapter";
|
||||||
private static final String UNDEFINED_PROGRESS = "--.-%";
|
private static final String UNDEFINED_PROGRESS = "--.-%";
|
||||||
|
private static final String DEFAULT_MIME_TYPE = "*/*";
|
||||||
|
|
||||||
|
|
||||||
static {
|
static {
|
||||||
@ -85,9 +98,12 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||||||
private ArrayList<ViewHolderItem> mPendingDownloadsItems = new ArrayList<>();
|
private ArrayList<ViewHolderItem> mPendingDownloadsItems = new ArrayList<>();
|
||||||
private Handler mHandler;
|
private Handler mHandler;
|
||||||
private MenuItem mClear;
|
private MenuItem mClear;
|
||||||
|
private MenuItem mStartButton;
|
||||||
|
private MenuItem mPauseButton;
|
||||||
private View mEmptyMessage;
|
private View mEmptyMessage;
|
||||||
|
private RecoverHelper mRecover;
|
||||||
|
|
||||||
public MissionAdapter(Context context, DownloadManager downloadManager, MenuItem clearButton, View emptyMessage) {
|
public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage) {
|
||||||
mContext = context;
|
mContext = context;
|
||||||
mDownloadManager = downloadManager;
|
mDownloadManager = downloadManager;
|
||||||
mDeleter = null;
|
mDeleter = null;
|
||||||
@ -105,10 +121,18 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||||||
onServiceMessage(msg);
|
onServiceMessage(msg);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mStartButton != null && mPauseButton != null) switch (msg.what) {
|
||||||
|
case DownloadManagerService.MESSAGE_DELETED:
|
||||||
|
case DownloadManagerService.MESSAGE_ERROR:
|
||||||
|
case DownloadManagerService.MESSAGE_FINISHED:
|
||||||
|
case DownloadManagerService.MESSAGE_PAUSED:
|
||||||
|
checkMasterButtonsVisibility();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mClear = clearButton;
|
|
||||||
mEmptyMessage = emptyMessage;
|
mEmptyMessage = emptyMessage;
|
||||||
|
|
||||||
mIterator = downloadManager.getIterator();
|
mIterator = downloadManager.getIterator();
|
||||||
@ -137,7 +161,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();
|
||||||
@ -170,10 +198,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));
|
||||||
|
|
||||||
@ -225,8 +253,10 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||||||
long deltaDone = mission.done - h.lastDone;
|
long deltaDone = mission.done - h.lastDone;
|
||||||
boolean hasError = mission.errCode != ERROR_NOTHING;
|
boolean hasError = mission.errCode != ERROR_NOTHING;
|
||||||
|
|
||||||
// on error hide marquee or show if condition (mission.done < 1 || mission.unknownLength) is true
|
// hide on error
|
||||||
h.progress.setMarquee(!hasError && (mission.done < 1 || mission.unknownLength));
|
// show if current resource length is not fetched
|
||||||
|
// show if length is unknown
|
||||||
|
h.progress.setMarquee(!hasError && (!mission.isInitialized() || mission.unknownLength));
|
||||||
|
|
||||||
float progress;
|
float progress;
|
||||||
if (mission.unknownLength) {
|
if (mission.unknownLength) {
|
||||||
@ -252,7 +282,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;
|
||||||
@ -305,36 +335,78 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean viewWithFileProvider(@NonNull File file) {
|
private void viewWithFileProvider(Mission mission) {
|
||||||
if (!file.exists()) return true;
|
if (checkInvalidFile(mission)) return;
|
||||||
|
|
||||||
String ext = Utility.getFileExt(file.getName());
|
String mimeType = resolveMimeType(mission);
|
||||||
if (ext == null) return false;
|
|
||||||
|
|
||||||
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1));
|
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(mContext, BuildConfig.APPLICATION_ID + ".provider", file);
|
Uri uri;
|
||||||
|
|
||||||
|
if (mission.storage.isDirect()) {
|
||||||
|
uri = FileProvider.getUriForFile(
|
||||||
|
mContext,
|
||||||
|
BuildConfig.APPLICATION_ID + ".provider",
|
||||||
|
new File(URI.create(mission.storage.getUri().toString()))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
uri = mission.storage.getUri();
|
||||||
|
}
|
||||||
|
|
||||||
Intent intent = new Intent();
|
Intent intent = new Intent();
|
||||||
intent.setAction(Intent.ACTION_VIEW);
|
intent.setAction(Intent.ACTION_VIEW);
|
||||||
intent.setDataAndType(uri, mimeType);
|
intent.setDataAndType(uri, mimeType);
|
||||||
intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION);
|
intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
intent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION);
|
intent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION);
|
||||||
}
|
}
|
||||||
if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
|
||||||
intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
|
intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
|
||||||
}
|
}
|
||||||
|
|
||||||
//mContext.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
//mContext.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
Log.v(TAG, "Starting intent: " + intent);
|
|
||||||
if (intent.resolveActivity(mContext.getPackageManager()) != null) {
|
if (intent.resolveActivity(mContext.getPackageManager()) != null) {
|
||||||
mContext.startActivity(intent);
|
mContext.startActivity(intent);
|
||||||
} else {
|
} else {
|
||||||
Toast noPlayerToast = Toast.makeText(mContext, R.string.toast_no_player, Toast.LENGTH_LONG);
|
Toast.makeText(mContext, R.string.toast_no_player, Toast.LENGTH_LONG).show();
|
||||||
noPlayerToast.show();
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void shareFile(Mission mission) {
|
||||||
|
if (checkInvalidFile(mission)) return;
|
||||||
|
|
||||||
|
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||||
|
intent.setType(resolveMimeType(mission));
|
||||||
|
intent.putExtra(Intent.EXTRA_STREAM, mission.storage.getUri());
|
||||||
|
|
||||||
|
mContext.startActivity(Intent.createChooser(intent, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String resolveMimeType(@NonNull Mission mission) {
|
||||||
|
String mimeType;
|
||||||
|
|
||||||
|
if (!mission.storage.isInvalid()) {
|
||||||
|
mimeType = mission.storage.getType();
|
||||||
|
if (mimeType != null && mimeType.length() > 0 && !mimeType.equals(StoredFileHelper.DEFAULT_MIME))
|
||||||
|
return mimeType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String ext = Utility.getFileExt(mission.storage.getName());
|
||||||
|
if (ext == null) return DEFAULT_MIME_TYPE;
|
||||||
|
|
||||||
|
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1));
|
||||||
|
|
||||||
|
return mimeType == null ? DEFAULT_MIME_TYPE : mimeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean checkInvalidFile(@NonNull Mission mission) {
|
||||||
|
if (mission.storage.existsAsFile()) return false;
|
||||||
|
|
||||||
|
Toast.makeText(mContext, R.string.missing_file, Toast.LENGTH_SHORT).show();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,15 +415,9 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void onServiceMessage(@NonNull Message msg) {
|
private void onServiceMessage(@NonNull Message msg) {
|
||||||
switch (msg.what) {
|
if (msg.what == DownloadManagerService.MESSAGE_PROGRESS) {
|
||||||
case DownloadManagerService.MESSAGE_PROGRESS:
|
setAutoRefresh(true);
|
||||||
setAutoRefresh(true);
|
return;
|
||||||
return;
|
|
||||||
case DownloadManagerService.MESSAGE_ERROR:
|
|
||||||
case DownloadManagerService.MESSAGE_FINISHED:
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < mPendingDownloadsItems.size(); i++) {
|
for (int i = 0; i < mPendingDownloadsItems.size(); i++) {
|
||||||
@ -370,74 +436,104 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void showError(@NonNull DownloadMission mission) {
|
private void showError(@NonNull DownloadMission mission) {
|
||||||
StringBuilder str = new StringBuilder();
|
@StringRes int msg = R.string.general_error;
|
||||||
str.append(mContext.getString(R.string.label_code));
|
String msgEx = null;
|
||||||
str.append(": ");
|
|
||||||
str.append(mission.errCode);
|
|
||||||
str.append('\n');
|
|
||||||
|
|
||||||
switch (mission.errCode) {
|
switch (mission.errCode) {
|
||||||
case 416:
|
case 416:
|
||||||
str.append(mContext.getString(R.string.error_http_requested_range_not_satisfiable));
|
msg = R.string.error_http_requested_range_not_satisfiable;
|
||||||
break;
|
break;
|
||||||
case 404:
|
case 404:
|
||||||
str.append(mContext.getString(R.string.error_http_not_found));
|
msg = R.string.error_http_not_found;
|
||||||
break;
|
break;
|
||||||
case ERROR_NOTHING:
|
case ERROR_NOTHING:
|
||||||
str.append("¿?");
|
return;// this never should happen
|
||||||
break;
|
|
||||||
case ERROR_FILE_CREATION:
|
case ERROR_FILE_CREATION:
|
||||||
str.append(mContext.getString(R.string.error_file_creation));
|
msg = R.string.error_file_creation;
|
||||||
break;
|
break;
|
||||||
case ERROR_HTTP_NO_CONTENT:
|
case ERROR_HTTP_NO_CONTENT:
|
||||||
str.append(mContext.getString(R.string.error_http_no_content));
|
msg = R.string.error_http_no_content;
|
||||||
break;
|
break;
|
||||||
case ERROR_HTTP_UNSUPPORTED_RANGE:
|
case ERROR_HTTP_UNSUPPORTED_RANGE:
|
||||||
str.append(mContext.getString(R.string.error_http_unsupported_range));
|
msg = R.string.error_http_unsupported_range;
|
||||||
break;
|
break;
|
||||||
case ERROR_PATH_CREATION:
|
case ERROR_PATH_CREATION:
|
||||||
str.append(mContext.getString(R.string.error_path_creation));
|
msg = R.string.error_path_creation;
|
||||||
break;
|
break;
|
||||||
case ERROR_PERMISSION_DENIED:
|
case ERROR_PERMISSION_DENIED:
|
||||||
str.append(mContext.getString(R.string.permission_denied));
|
msg = R.string.permission_denied;
|
||||||
break;
|
break;
|
||||||
case ERROR_SSL_EXCEPTION:
|
case ERROR_SSL_EXCEPTION:
|
||||||
str.append(mContext.getString(R.string.error_ssl_exception));
|
msg = R.string.error_ssl_exception;
|
||||||
break;
|
break;
|
||||||
case ERROR_UNKNOWN_HOST:
|
case ERROR_UNKNOWN_HOST:
|
||||||
str.append(mContext.getString(R.string.error_unknown_host));
|
msg = R.string.error_unknown_host;
|
||||||
break;
|
break;
|
||||||
case ERROR_CONNECT_HOST:
|
case ERROR_CONNECT_HOST:
|
||||||
str.append(mContext.getString(R.string.error_connect_host));
|
msg = R.string.error_connect_host;
|
||||||
|
break;
|
||||||
|
case ERROR_POSTPROCESSING_STOPPED:
|
||||||
|
msg = R.string.error_postprocessing_stopped;
|
||||||
break;
|
break;
|
||||||
case ERROR_POSTPROCESSING:
|
case ERROR_POSTPROCESSING:
|
||||||
str.append(mContext.getString(R.string.error_postprocessing_failed));
|
case ERROR_POSTPROCESSING_HOLD:
|
||||||
|
showError(mission.errObject, UserAction.DOWNLOAD_POSTPROCESSING, R.string.error_postprocessing_failed);
|
||||||
|
return;
|
||||||
|
case ERROR_INSUFFICIENT_STORAGE:
|
||||||
|
msg = R.string.error_insufficient_storage;
|
||||||
|
break;
|
||||||
case ERROR_UNKNOWN_EXCEPTION:
|
case ERROR_UNKNOWN_EXCEPTION:
|
||||||
|
showError(mission.errObject, UserAction.DOWNLOAD_FAILED, R.string.general_error);
|
||||||
|
return;
|
||||||
|
case ERROR_PROGRESS_LOST:
|
||||||
|
msg = R.string.error_progress_lost;
|
||||||
|
break;
|
||||||
|
case ERROR_TIMEOUT:
|
||||||
|
msg = R.string.error_timeout;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (mission.errCode >= 100 && mission.errCode < 600) {
|
if (mission.errCode >= 100 && mission.errCode < 600) {
|
||||||
str = new StringBuilder(8);
|
msgEx = "HTTP " + mission.errCode;
|
||||||
str.append("HTTP ");
|
|
||||||
str.append(mission.errCode);
|
|
||||||
} else if (mission.errObject == null) {
|
} else if (mission.errObject == null) {
|
||||||
str.append("(not_decelerated_error_code)");
|
msgEx = "(not_decelerated_error_code)";
|
||||||
|
} else {
|
||||||
|
showError(mission.errObject, UserAction.DOWNLOAD_FAILED, msg);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mission.errObject != null) {
|
AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
|
||||||
str.append("\n\n");
|
|
||||||
str.append(mission.errObject.toString());
|
if (msgEx != null)
|
||||||
|
builder.setMessage(msgEx);
|
||||||
|
else
|
||||||
|
builder.setMessage(msg);
|
||||||
|
|
||||||
|
// add report button for non-HTTP errors (range 100-599)
|
||||||
|
if (mission.errObject != null && (mission.errCode < 100 || mission.errCode >= 600)) {
|
||||||
|
@StringRes final int mMsg = msg;
|
||||||
|
builder.setPositiveButton(R.string.error_report_title, (dialog, which) ->
|
||||||
|
showError(mission.errObject, UserAction.DOWNLOAD_FAILED, mMsg)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
|
builder.setNegativeButton(android.R.string.ok, (dialog, which) -> dialog.cancel())
|
||||||
builder.setTitle(mission.name)
|
.setTitle(mission.storage.getName())
|
||||||
.setMessage(str)
|
|
||||||
.setNegativeButton(android.R.string.ok, (dialog, which) -> dialog.cancel())
|
|
||||||
.create()
|
.create()
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void showError(Exception exception, UserAction action, @StringRes int reason) {
|
||||||
|
ErrorActivity.reportError(
|
||||||
|
mContext,
|
||||||
|
Collections.singletonList(exception),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
ErrorActivity.ErrorInfo.make(action, "-", "-", reason)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public void clearFinishedDownloads() {
|
public void clearFinishedDownloads() {
|
||||||
mDownloadManager.forgetFinishedDownloads();
|
mDownloadManager.forgetFinishedDownloads();
|
||||||
applyChanges();
|
applyChanges();
|
||||||
@ -466,16 +562,33 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||||||
showError(mission);
|
showError(mission);
|
||||||
return true;
|
return true;
|
||||||
case R.id.queue:
|
case R.id.queue:
|
||||||
h.queue.setChecked(!h.queue.isChecked());
|
boolean flag = !h.queue.isChecked();
|
||||||
mission.enqueued = h.queue.isChecked();
|
h.queue.setChecked(flag);
|
||||||
|
mission.setEnqueued(flag);
|
||||||
updateProgress(h);
|
updateProgress(h);
|
||||||
return true;
|
return true;
|
||||||
|
case R.id.retry:
|
||||||
|
if (mission.hasInvalidStorage()) {
|
||||||
|
mDownloadManager.tryRecover(mission);
|
||||||
|
if (mission.storage.isInvalid())
|
||||||
|
mRecover.tryRecover(mission);
|
||||||
|
else
|
||||||
|
recoverMission(mission);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
mission.psContinue(true);
|
||||||
|
return true;
|
||||||
|
case R.id.cancel:
|
||||||
|
mission.psContinue(false);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case R.id.open:
|
case R.id.menu_item_share:
|
||||||
return viewWithFileProvider(h.item.mission.getDownloadedFile());
|
shareFile(h.item.mission);
|
||||||
|
return true;
|
||||||
case R.id.delete:
|
case R.id.delete:
|
||||||
if (mDeleter == null) {
|
if (mDeleter == null) {
|
||||||
mDownloadManager.deleteMission(h.item.mission);
|
mDownloadManager.deleteMission(h.item.mission);
|
||||||
@ -486,7 +599,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));
|
||||||
@ -529,29 +642,74 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setClearButton(MenuItem clearButton) {
|
public void setClearButton(MenuItem clearButton) {
|
||||||
if (mClear == null) clearButton.setVisible(mIterator.hasFinishedMissions());
|
if (mClear == null)
|
||||||
|
clearButton.setVisible(mIterator.hasFinishedMissions());
|
||||||
|
|
||||||
mClear = clearButton;
|
mClear = clearButton;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setMasterButtons(MenuItem startButton, MenuItem pauseButton) {
|
||||||
|
boolean init = mStartButton == null || mPauseButton == null;
|
||||||
|
|
||||||
|
mStartButton = startButton;
|
||||||
|
mPauseButton = pauseButton;
|
||||||
|
|
||||||
|
if (init) checkMasterButtonsVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
private void checkEmptyMessageVisibility() {
|
private void checkEmptyMessageVisibility() {
|
||||||
int flag = mIterator.getOldListSize() > 0 ? View.GONE : View.VISIBLE;
|
int flag = mIterator.getOldListSize() > 0 ? View.GONE : View.VISIBLE;
|
||||||
if (mEmptyMessage.getVisibility() != flag) mEmptyMessage.setVisibility(flag);
|
if (mEmptyMessage.getVisibility() != flag) mEmptyMessage.setVisibility(flag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void checkMasterButtonsVisibility() {
|
||||||
|
boolean[] state = mIterator.hasValidPendingMissions();
|
||||||
|
|
||||||
public void deleterDispose(Bundle bundle) {
|
mStartButton.setVisible(state[0]);
|
||||||
if (mDeleter != null) mDeleter.dispose(bundle);
|
mPauseButton.setVisible(state[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleterLoad(Bundle bundle, View view) {
|
public void ensurePausedMissions() {
|
||||||
|
for (ViewHolderItem h : mPendingDownloadsItems) {
|
||||||
|
if (((DownloadMission) h.item.mission).running) continue;
|
||||||
|
updateProgress(h);
|
||||||
|
h.lastTimeStamp = -1;
|
||||||
|
h.lastDone = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void deleterDispose(boolean commitChanges) {
|
||||||
|
if (mDeleter != null) mDeleter.dispose(commitChanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
for (ViewHolderItem h : mPendingDownloadsItems) {
|
||||||
|
if (mission != h.item.mission) continue;
|
||||||
|
|
||||||
|
mission.errObject = null;
|
||||||
|
mission.resetState(true, false, DownloadMission.ERROR_NOTHING);
|
||||||
|
|
||||||
|
h.status.setText(UNDEFINED_PROGRESS);
|
||||||
|
h.state = -1;
|
||||||
|
h.size.setText(Utility.formatBytes(mission.getLength()));
|
||||||
|
h.progress.setMarquee(true);
|
||||||
|
|
||||||
|
mDownloadManager.resumeMission(mission);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private boolean mUpdaterRunning = false;
|
private boolean mUpdaterRunning = false;
|
||||||
private final Runnable rUpdater = this::updater;
|
private final Runnable rUpdater = this::updater;
|
||||||
@ -593,6 +751,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;
|
||||||
@ -604,6 +766,8 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||||||
ProgressDrawable progress;
|
ProgressDrawable progress;
|
||||||
|
|
||||||
PopupMenu popupMenu;
|
PopupMenu popupMenu;
|
||||||
|
MenuItem retry;
|
||||||
|
MenuItem cancel;
|
||||||
MenuItem start;
|
MenuItem start;
|
||||||
MenuItem pause;
|
MenuItem pause;
|
||||||
MenuItem open;
|
MenuItem open;
|
||||||
@ -636,22 +800,34 @@ public class MissionAdapter extends Adapter<ViewHolder> {
|
|||||||
button.setOnClickListener(v -> showPopupMenu());
|
button.setOnClickListener(v -> showPopupMenu());
|
||||||
|
|
||||||
Menu menu = popupMenu.getMenu();
|
Menu menu = popupMenu.getMenu();
|
||||||
|
retry = menu.findItem(R.id.retry);
|
||||||
|
cancel = menu.findItem(R.id.cancel);
|
||||||
start = menu.findItem(R.id.start);
|
start = menu.findItem(R.id.start);
|
||||||
pause = menu.findItem(R.id.pause);
|
pause = menu.findItem(R.id.pause);
|
||||||
open = menu.findItem(R.id.open);
|
open = menu.findItem(R.id.menu_item_share);
|
||||||
queue = menu.findItem(R.id.queue);
|
queue = menu.findItem(R.id.queue);
|
||||||
showError = menu.findItem(R.id.error_message_view);
|
showError = menu.findItem(R.id.error_message_view);
|
||||||
delete = menu.findItem(R.id.delete);
|
delete = menu.findItem(R.id.delete);
|
||||||
source = menu.findItem(R.id.source);
|
source = menu.findItem(R.id.source);
|
||||||
checksum = menu.findItem(R.id.checksum);
|
checksum = menu.findItem(R.id.checksum);
|
||||||
|
|
||||||
itemView.setOnClickListener((v) -> {
|
itemView.setHapticFeedbackEnabled(true);
|
||||||
|
|
||||||
|
itemView.setOnClickListener(v -> {
|
||||||
if (item.mission instanceof FinishedMission)
|
if (item.mission instanceof FinishedMission)
|
||||||
viewWithFileProvider(item.mission.getDownloadedFile());
|
viewWithFileProvider(item.mission);
|
||||||
|
});
|
||||||
|
|
||||||
|
itemView.setOnLongClickListener(v -> {
|
||||||
|
v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
||||||
|
showPopupMenu();
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showPopupMenu() {
|
private void showPopupMenu() {
|
||||||
|
retry.setVisible(false);
|
||||||
|
cancel.setVisible(false);
|
||||||
start.setVisible(false);
|
start.setVisible(false);
|
||||||
pause.setVisible(false);
|
pause.setVisible(false);
|
||||||
open.setVisible(false);
|
open.setVisible(false);
|
||||||
@ -664,7 +840,20 @@ 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.setVisible(true);
|
||||||
|
delete.setVisible(true);
|
||||||
|
showError.setVisible(true);
|
||||||
|
} else if (mission.isPsRunning()) {
|
||||||
|
switch (mission.errCode) {
|
||||||
|
case ERROR_INSUFFICIENT_STORAGE:
|
||||||
|
case ERROR_POSTPROCESSING_HOLD:
|
||||||
|
retry.setVisible(true);
|
||||||
|
cancel.setVisible(true);
|
||||||
|
showError.setVisible(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (mission.running) {
|
if (mission.running) {
|
||||||
pause.setVisible(true);
|
pause.setVisible(true);
|
||||||
} else {
|
} else {
|
||||||
@ -713,7 +902,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;
|
||||||
|
|
||||||
@ -736,8 +925,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
|
||||||
@ -764,4 +953,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);
|
|
||||||
items = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String[] names = new String[items.size()];
|
for (Mission mission : items) mDownloadManager.deleteMission(mission);
|
||||||
String[] locations = new String[items.size()];
|
items = null;
|
||||||
|
|
||||||
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,7 @@
|
|||||||
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.app.AlertDialog;
|
||||||
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 +10,8 @@ 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.annotation.NonNull;
|
||||||
|
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,23 +20,31 @@ 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;
|
||||||
private MenuItem mSwitch;
|
private MenuItem mSwitch;
|
||||||
private MenuItem mClear = null;
|
private MenuItem mClear = null;
|
||||||
|
private MenuItem mStart = null;
|
||||||
|
private MenuItem mPause = null;
|
||||||
|
|
||||||
private RecyclerView mList;
|
private RecyclerView mList;
|
||||||
private View mEmpty;
|
private View mEmpty;
|
||||||
@ -43,21 +53,24 @@ 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(), mClear, mEmpty);
|
mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty);
|
||||||
mAdapter.deleterLoad(mBundle, getView());
|
mAdapter.deleterLoad(getView());
|
||||||
|
|
||||||
mBundle = null;
|
mAdapter.setRecover(MissionsFragment.this::recoverMission);
|
||||||
|
|
||||||
|
setAdapterButtons();
|
||||||
|
|
||||||
mBinder.addMissionEventListener(mAdapter.getMessenger());
|
mBinder.addMissionEventListener(mAdapter.getMessenger());
|
||||||
mBinder.enableNotifications(false);
|
mBinder.enableNotifications(false);
|
||||||
@ -74,15 +87,12 @@ public class MissionsFragment extends Fragment {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
View v = inflater.inflate(R.layout.missions, container, false);
|
View v = inflater.inflate(R.layout.missions, container, false);
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
@ -132,7 +142,7 @@ public class MissionsFragment extends Fragment {
|
|||||||
public void onAttach(Activity activity) {
|
public void onAttach(Activity activity) {
|
||||||
super.onAttach(activity);
|
super.onAttach(activity);
|
||||||
|
|
||||||
mContext = activity.getApplicationContext();
|
mContext = activity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -144,7 +154,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;
|
||||||
@ -154,7 +164,11 @@ public class MissionsFragment extends Fragment {
|
|||||||
public void onPrepareOptionsMenu(Menu menu) {
|
public void onPrepareOptionsMenu(Menu menu) {
|
||||||
mSwitch = menu.findItem(R.id.switch_mode);
|
mSwitch = menu.findItem(R.id.switch_mode);
|
||||||
mClear = menu.findItem(R.id.clear_list);
|
mClear = menu.findItem(R.id.clear_list);
|
||||||
if (mAdapter != null) mAdapter.setClearButton(mClear);
|
mStart = menu.findItem(R.id.start_downloads);
|
||||||
|
mPause = menu.findItem(R.id.pause_downloads);
|
||||||
|
|
||||||
|
if (mAdapter != null) setAdapterButtons();
|
||||||
|
|
||||||
super.onPrepareOptionsMenu(menu);
|
super.onPrepareOptionsMenu(menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,8 +180,23 @@ public class MissionsFragment extends Fragment {
|
|||||||
updateList();
|
updateList();
|
||||||
return true;
|
return true;
|
||||||
case R.id.clear_list:
|
case R.id.clear_list:
|
||||||
mAdapter.clearFinishedDownloads();
|
AlertDialog.Builder prompt = new AlertDialog.Builder(mContext);
|
||||||
|
prompt.setTitle(R.string.clear_finished_download);
|
||||||
|
prompt.setMessage(R.string.confirm_prompt);
|
||||||
|
prompt.setPositiveButton(android.R.string.ok, (dialog, which) -> mAdapter.clearFinishedDownloads());
|
||||||
|
prompt.setNegativeButton(R.string.cancel, null);
|
||||||
|
prompt.create().show();
|
||||||
return true;
|
return true;
|
||||||
|
case R.id.start_downloads:
|
||||||
|
item.setVisible(false);
|
||||||
|
mPause.setVisible(true);
|
||||||
|
mBinder.getDownloadManager().startAllMissions();
|
||||||
|
return true;
|
||||||
|
case R.id.pause_downloads:
|
||||||
|
item.setVisible(false);
|
||||||
|
mStart.setVisible(true);
|
||||||
|
mBinder.getDownloadManager().pauseAllMissions(false);
|
||||||
|
mAdapter.ensurePausedMissions();// update items view
|
||||||
default:
|
default:
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
@ -193,9 +222,9 @@ public class MissionsFragment extends Fragment {
|
|||||||
int icon;
|
int icon;
|
||||||
|
|
||||||
if (mLinear)
|
if (mLinear)
|
||||||
icon = isLight ? R.drawable.ic_list_black_24dp : R.drawable.ic_list_white_24dp;
|
|
||||||
else
|
|
||||||
icon = isLight ? R.drawable.ic_grid_black_24dp : R.drawable.ic_grid_white_24dp;
|
icon = isLight ? R.drawable.ic_grid_black_24dp : R.drawable.ic_grid_white_24dp;
|
||||||
|
else
|
||||||
|
icon = isLight ? R.drawable.ic_list_black_24dp : R.drawable.ic_list_white_24dp;
|
||||||
|
|
||||||
mSwitch.setIcon(icon);
|
mSwitch.setIcon(icon);
|
||||||
mSwitch.setTitle(mLinear ? R.string.grid : R.string.list);
|
mSwitch.setTitle(mLinear ? R.string.grid : R.string.list);
|
||||||
@ -203,12 +232,29 @@ public class MissionsFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setAdapterButtons() {
|
||||||
|
if (mClear == null || mStart == null || mPause == null) return;
|
||||||
|
|
||||||
|
mAdapter.setClearButton(mClear);
|
||||||
|
mAdapter.setMasterButtons(mStart, mPause);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recoverMission(@NonNull DownloadMission mission) {
|
||||||
|
unsafeMissionTarget = mission;
|
||||||
|
StoredFileHelper.requestSafWithFileCreation(
|
||||||
|
MissionsFragment.this,
|
||||||
|
REQUEST_DOWNLOAD_PATH_SAF,
|
||||||
|
mission.storage.getName(),
|
||||||
|
mission.storage.getType()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(Bundle outState) {
|
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||||
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());
|
||||||
}
|
}
|
||||||
@ -237,4 +283,23 @@ 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 {
|
||||||
|
String tag = unsafeMissionTarget.storage.getTag();
|
||||||
|
unsafeMissionTarget.storage = new StoredFileHelper(mContext, null, data.getData(), tag);
|
||||||
|
mAdapter.recoverMission(unsafeMissionTarget);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Toast.makeText(mContext, R.string.general_error, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,14 +9,15 @@ import android.support.annotation.DrawableRes;
|
|||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.content.ContextCompat;
|
import android.support.v4.content.ContextCompat;
|
||||||
|
import android.util.Log;
|
||||||
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 +26,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 {
|
||||||
|
|
||||||
@ -80,6 +82,7 @@ public class Utility {
|
|||||||
objectInputStream = new ObjectInputStream(new FileInputStream(file));
|
objectInputStream = new ObjectInputStream(new FileInputStream(file));
|
||||||
object = (T) objectInputStream.readObject();
|
object = (T) objectInputStream.readObject();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
Log.e("Utility", "Failed to deserialize the object", e);
|
||||||
object = null;
|
object = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,7 +209,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 +218,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 +250,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) {
|
||||||
@ -264,8 +267,7 @@ public class Utility {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
long length = Long.parseLong(connection.getHeaderField("Content-Length"));
|
return Long.parseLong(connection.getHeaderField("Content-Length"));
|
||||||
if (length >= 0) return length;
|
|
||||||
} catch (Exception err) {
|
} catch (Exception err) {
|
||||||
// nothing to do
|
// nothing to do
|
||||||
}
|
}
|
||||||
|
BIN
app/src/main/res/drawable-hdpi/ic_pause_black_24dp.png
Normal file
After Width: | Height: | Size: 135 B |
BIN
app/src/main/res/drawable-hdpi/ic_pause_white_24dp.png
Normal file
After Width: | Height: | Size: 138 B |
BIN
app/src/main/res/drawable-mdpi/ic_pause_black_24dp.png
Normal file
After Width: | Height: | Size: 109 B |
BIN
app/src/main/res/drawable-mdpi/ic_pause_white_24dp.png
Normal file
After Width: | Height: | Size: 112 B |
BIN
app/src/main/res/drawable-xhdpi/ic_pause_black_24dp.png
Normal file
After Width: | Height: | Size: 162 B |
BIN
app/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png
Normal file
After Width: | Height: | Size: 139 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_pause_black_24dp.png
Normal file
After Width: | Height: | Size: 196 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png
Normal file
After Width: | Height: | Size: 206 B |