From 4e1423d224296e89551636ec5e02ccc94ef7d421 Mon Sep 17 00:00:00 2001 From: Vasiliy Date: Sat, 13 Apr 2019 10:31:32 +0300 Subject: [PATCH 01/30] Implement playback state management --- .../org/schabi/newpipe/RouterActivity.java | 8 +- .../history/dao/StreamHistoryDAO.java | 5 + .../stream/model/StreamStateEntity.java | 14 + .../fragments/detail/VideoDetailFragment.java | 64 +- .../fragments/list/BaseListFragment.java | 6 +- .../list/channel/ChannelFragment.java | 16 +- .../list/playlist/PlaylistFragment.java | 22 +- .../local/history/HistoryRecordManager.java | 49 +- .../history/StatisticsPlaylistFragment.java | 16 +- .../local/playlist/LocalPlaylistFragment.java | 16 +- .../newpipe/player/BackgroundPlayer.java | 1 + .../org/schabi/newpipe/player/BasePlayer.java | 71 +- .../newpipe/player/MainVideoPlayer.java | 12 +- .../newpipe/player/PopupVideoPlayer.java | 4 +- .../newpipe/player/ServicePlayerActivity.java | 3 +- .../schabi/newpipe/player/VideoPlayer.java | 5 + .../util/CommentTextOnTouchListener.java | 2 +- .../schabi/newpipe/util/NavigationHelper.java | 47 +- .../newpipe/views/AnimatedProgressBar.java | 69 ++ .../progress_soundcloud_horizontal_dark.xml | 15 + .../progress_soundcloud_horizontal_light.xml | 15 + .../progress_youtube_horizontal_dark.xml | 15 + .../progress_youtube_horizontal_light.xml | 15 + .../fragment_video_detail.xml | 77 +- .../main/res/layout/fragment_video_detail.xml | 913 +++++++++--------- app/src/main/res/values-ru/strings.xml | 5 +- app/src/main/res/values-uk/strings.xml | 5 +- app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/settings_keys.xml | 1 + app/src/main/res/values/strings.xml | 5 +- app/src/main/res/values/styles.xml | 2 + app/src/main/res/values/styles_services.xml | 3 + app/src/main/res/xml/history_settings.xml | 58 +- 33 files changed, 978 insertions(+), 582 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/views/AnimatedProgressBar.java create mode 100644 app/src/main/res/drawable/progress_soundcloud_horizontal_dark.xml create mode 100644 app/src/main/res/drawable/progress_soundcloud_horizontal_light.xml create mode 100644 app/src/main/res/drawable/progress_youtube_horizontal_dark.xml create mode 100644 app/src/main/res/drawable/progress_youtube_horizontal_light.xml diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index f040dc8b4..c7bf4c881 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -574,7 +574,7 @@ public class RouterActivity extends AppCompatActivity { playQueue = new SinglePlayQueue((StreamInfo) info); if (playerChoice.equals(videoPlayerKey)) { - NavigationHelper.playOnMainPlayer(this, playQueue); + NavigationHelper.playOnMainPlayer(this, playQueue, true); } else if (playerChoice.equals(backgroundPlayerKey)) { NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true); } 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); if (playerChoice.equals(videoPlayerKey)) { - NavigationHelper.playOnMainPlayer(this, playQueue); + NavigationHelper.playOnMainPlayer(this, playQueue, true); } else if (playerChoice.equals(backgroundPlayerKey)) { - NavigationHelper.playOnBackgroundPlayer(this, playQueue); + NavigationHelper.playOnBackgroundPlayer(this, playQueue, true); } else if (playerChoice.equals(popupPlayerKey)) { - NavigationHelper.playOnPopupPlayer(this, playQueue); + NavigationHelper.playOnPopupPlayer(this, playQueue, true); } } }; diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java index 847153e12..50d723f1f 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java @@ -50,6 +50,11 @@ public abstract class StreamHistoryDAO implements HistoryDAO> 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") public abstract int deleteStreamHistory(final long streamId); diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java index 15940a964..946ee1182 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java @@ -5,6 +5,8 @@ import android.arch.persistence.room.ColumnInfo; import android.arch.persistence.room.Entity; import android.arch.persistence.room.ForeignKey; +import java.util.concurrent.TimeUnit; + 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.STREAM_STATE_TABLE; @@ -22,6 +24,12 @@ public class StreamStateEntity { final public static String JOIN_STREAM_ID = "stream_id"; 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) private long streamUid; @@ -48,4 +56,10 @@ public class StreamStateEntity { public void setProgressTime(long 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; + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index bbd1a315d..d6630c9c3 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -89,12 +89,14 @@ import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.StreamItemAdapter; import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; +import org.schabi.newpipe.views.AnimatedProgressBar; import java.io.Serializable; import java.util.Collection; import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.concurrent.TimeUnit; import icepick.State; import io.reactivex.Single; @@ -118,7 +120,7 @@ public class VideoDetailFragment private static final int RELATED_STREAMS_UPDATE_FLAG = 0x1; private static final int RESOLUTIONS_MENU_UPDATE_FLAG = 0x2; 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 showRelatedStreams; @@ -136,6 +138,8 @@ public class VideoDetailFragment private Disposable currentWorker; @NonNull private CompositeDisposable disposables = new CompositeDisposable(); + @Nullable + private Disposable positionSubscriber = null; private List sortedVideoStreams; private int selectedVideoStreamIndex = -1; @@ -153,6 +157,7 @@ public class VideoDetailFragment private View thumbnailBackgroundButton; private ImageView thumbnailImageView; private ImageView thumbnailPlayButton; + private AnimatedProgressBar positionView; private View videoTitleRoot; private TextView videoTitleTextView; @@ -165,6 +170,7 @@ public class VideoDetailFragment private TextView detailControlsDownload; private TextView appendControlsDetail; private TextView detailDurationView; + private TextView detailPositionView; private LinearLayout videoDescriptionRootLayout; private TextView videoUploadDateView; @@ -259,6 +265,8 @@ public class VideoDetailFragment // Check if it was loading when the fragment was stopped/paused, if (wasLoading.getAndSet(false)) { selectAndLoadVideo(serviceId, url, name); + } else if (currentInfo != null) { + updateProgressInfo(currentInfo); } } @@ -268,8 +276,10 @@ public class VideoDetailFragment PreferenceManager.getDefaultSharedPreferences(activity) .unregisterOnSharedPreferenceChangeListener(this); + if (positionSubscriber != null) positionSubscriber.dispose(); if (currentWorker != null) currentWorker.dispose(); if (disposables != null) disposables.clear(); + positionSubscriber = null; currentWorker = null; disposables = null; } @@ -462,6 +472,7 @@ public class VideoDetailFragment videoTitleTextView = rootView.findViewById(R.id.detail_video_title_view); videoTitleToggleArrow = rootView.findViewById(R.id.detail_toggle_description_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); detailControlsPopup = rootView.findViewById(R.id.detail_controls_popup); @@ -469,6 +480,7 @@ public class VideoDetailFragment detailControlsDownload = rootView.findViewById(R.id.detail_controls_download); appendControlsDetail = rootView.findViewById(R.id.touch_append_detail); 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); videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view); @@ -536,10 +548,10 @@ public class VideoDetailFragment final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> { switch (i) { case 0: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item), true); break; case 1: - NavigationHelper.enqueueOnPopupPlayer(getActivity(), new SinglePlayQueue(item)); + NavigationHelper.enqueueOnPopupPlayer(getActivity(), new SinglePlayQueue(item), true); break; case 2: if (getFragmentManager() != null) { @@ -890,11 +902,11 @@ public class VideoDetailFragment final PlayQueue itemQueue = new SinglePlayQueue(currentInfo); if (append) { - NavigationHelper.enqueueOnPopupPlayer(activity, itemQueue); + NavigationHelper.enqueueOnPopupPlayer(activity, itemQueue, false); } else { Toast.makeText(activity, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); final Intent intent = NavigationHelper.getPlayerIntent( - activity, PopupVideoPlayer.class, itemQueue, getSelectedVideoStream().resolution + activity, PopupVideoPlayer.class, itemQueue, getSelectedVideoStream().resolution, true ); activity.startService(intent); } @@ -914,9 +926,9 @@ public class VideoDetailFragment private void openNormalBackgroundPlayer(final boolean append) { final PlayQueue itemQueue = new SinglePlayQueue(currentInfo); if (append) { - NavigationHelper.enqueueOnBackgroundPlayer(activity, itemQueue); + NavigationHelper.enqueueOnBackgroundPlayer(activity, itemQueue, false); } else { - NavigationHelper.playOnBackgroundPlayer(activity, itemQueue); + NavigationHelper.playOnBackgroundPlayer(activity, itemQueue, true); } } @@ -926,7 +938,7 @@ public class VideoDetailFragment mIntent = NavigationHelper.getPlayerIntent(activity, MainVideoPlayer.class, playQueue, - getSelectedVideoStream().getResolution()); + getSelectedVideoStream().getResolution(), true); startActivity(mIntent); } @@ -1032,6 +1044,8 @@ public class VideoDetailFragment animateView(spinnerToolbar, false, 200); animateView(thumbnailPlayButton, false, 50); animateView(detailDurationView, false, 100); + animateView(detailPositionView, false, 100); + animateView(positionView, false, 50); videoTitleTextView.setText(name != null ? name : ""); videoTitleTextView.setMaxLines(1); @@ -1146,6 +1160,7 @@ public class VideoDetailFragment videoUploadDateView.setText(Localization.localizeDate(activity, info.getUploadDate())); } prepareDescription(info.getDescription()); + updateProgressInfo(info); animateView(spinnerToolbar, true, 500); setupActionBar(info); @@ -1250,4 +1265,37 @@ public class VideoDetailFragment showError(getString(R.string.blocked_by_gema), false, R.drawable.gruese_die_gema); } + + 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.setProgress(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); + }); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index dbc3dd8a2..68784852e 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -266,13 +266,13 @@ public abstract class BaseListFragment extends BaseStateFragment implem final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { switch (i) { case 0: - NavigationHelper.playOnBackgroundPlayer(context, new SinglePlayQueue(item)); + NavigationHelper.playOnBackgroundPlayer(context, new SinglePlayQueue(item), true); break; case 1: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item), true); break; case 2: - NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item), true); break; case 3: if (getFragmentManager() != null) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 71865b04d..934e934e9 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -170,19 +170,19 @@ public class ChannelFragment extends BaseListInfoFragment { final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0); switch (i) { case 0: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item), false); break; case 1: - NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item), false); break; case 2: - NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); + NavigationHelper.playOnMainPlayer(context, getPlayQueue(index), true); break; case 3: - NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index)); + NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index), true); break; case 4: - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index), true); break; case 5: if (getFragmentManager() != null) { @@ -440,11 +440,11 @@ public class ChannelFragment extends BaseListInfoFragment { monitorSubscription(result); headerPlayAllButton.setOnClickListener( - view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); + view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false)); headerPopupButton.setOnClickListener( - view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); + view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false)); headerBackgroundButton.setOnClickListener( - view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); + view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false)); } private PlayQueue getPlayQueue() { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 2a775fe8f..77aa0a250 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -154,22 +154,22 @@ public class PlaylistFragment extends BaseListInfoFragment { final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0); switch (i) { case 0: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item), false); break; case 1: - NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item), false); break; case 2: - NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); + NavigationHelper.playOnMainPlayer(context, getPlayQueue(index), true); break; case 3: - NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index)); + NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index), true); break; case 4: - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index), true); break; case 5: - ShareUtils.shareUrl(this.getContext(), item.getName(), item.getUrl()); + ShareUtils.shareUrl(requireContext(), item.getName(), item.getUrl()); break; default: break; @@ -301,19 +301,19 @@ public class PlaylistFragment extends BaseListInfoFragment { .subscribe(getPlaylistBookmarkSubscriber()); headerPlayAllButton.setOnClickListener(view -> - NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); + NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false)); headerPopupButton.setOnClickListener(view -> - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false)); headerBackgroundButton.setOnClickListener(view -> - NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); + NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false)); headerPopupButton.setOnLongClickListener(view -> { - NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue()); + NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true); return true; }); headerBackgroundButton.setOnLongClickListener(view -> { - NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue()); + NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true); return true; }); } diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java index 56453773a..f9090128c 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java @@ -37,12 +37,14 @@ import org.schabi.newpipe.database.stream.dao.StreamStateDAO; import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.player.playqueue.PlayQueueItem; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.List; +import io.reactivex.Completable; import io.reactivex.Flowable; import io.reactivex.Maybe; import io.reactivex.Single; @@ -80,9 +82,9 @@ public class HistoryRecordManager { final Date currentTime = new Date(); return Maybe.fromCallable(() -> database.runInTransaction(() -> { 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); latestEntry.setAccessDate(currentTime); latestEntry.setRepeatCount(latestEntry.getRepeatCount() + 1); @@ -99,7 +101,7 @@ public class HistoryRecordManager { } public Single deleteWholeStreamHistory() { - return Single.fromCallable(() -> streamHistoryTable.deleteAll()) + return Single.fromCallable(streamHistoryTable::deleteAll) .subscribeOn(Schedulers.io()); } @@ -160,7 +162,7 @@ public class HistoryRecordManager { } public Single deleteWholeSearchHistory() { - return Single.fromCallable(() -> searchHistoryTable.deleteAll()) + return Single.fromCallable(searchHistoryTable::deleteAll) .subscribeOn(Schedulers.io()); } @@ -180,18 +182,41 @@ public class HistoryRecordManager { // Stream State History /////////////////////////////////////////////////////// - @SuppressWarnings("unused") - public Maybe loadStreamState(final StreamInfo info) { - return Maybe.fromCallable(() -> streamTable.upsert(new StreamEntity(info))) - .flatMap(streamId -> streamStateTable.getState(streamId).firstElement()) - .flatMap(states -> states.isEmpty() ? Maybe.empty() : Maybe.just(states.get(0))) + public Maybe getStreamHistory(final StreamInfo info) { + return Maybe.fromCallable(() -> { + final long streamId = streamTable.upsert(new StreamEntity(info)); + return streamHistoryTable.getLatestEntry(streamId); + }).subscribeOn(Schedulers.io()); + } + + public Maybe 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()); } - public Maybe saveStreamState(@NonNull final StreamInfo info, final long progressTime) { - return Maybe.fromCallable(() -> database.runInTransaction(() -> { + public Maybe loadStreamState(final StreamInfo info) { + 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)); - 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()); } diff --git a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java index 5a62a3969..b97fdf8f2 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java @@ -310,11 +310,11 @@ public class StatisticsPlaylistFragment } headerPlayAllButton.setOnClickListener(view -> - NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); + NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false)); headerPopupButton.setOnClickListener(view -> - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false)); headerBackgroundButton.setOnClickListener(view -> - NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); + NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false)); sortButton.setOnClickListener(view -> toggleSortMode()); hideLoading(); @@ -377,19 +377,19 @@ public class StatisticsPlaylistFragment final int index = Math.max(itemListAdapter.getItemsList().indexOf(item), 0); switch (i) { case 0: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(infoItem)); + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(infoItem), false); break; case 1: - NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(infoItem)); + NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(infoItem), false); break; case 2: - NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); + NavigationHelper.playOnMainPlayer(context, getPlayQueue(index), true); break; case 3: - NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index)); + NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index), true); break; case 4: - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index), true); break; case 5: deleteEntry(index); diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index dc101fade..501016642 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -319,11 +319,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment - NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); + NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false)); headerPopupButton.setOnClickListener(view -> - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false)); headerBackgroundButton.setOnClickListener(view -> - NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); + NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false)); hideLoading(); } @@ -534,20 +534,20 @@ public class LocalPlaylistFragment extends BaseLocalListFragment 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... initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, /*playOnInit=*/true); @@ -615,6 +634,9 @@ public abstract class BasePlayer implements break; case Player.STATE_ENDED: // 4 changeState(STATE_COMPLETED); + if (currentMetadata != null) { + resetPlaybackState(currentMetadata.getMetadata()); + } isPrepared = false; break; } @@ -721,6 +743,7 @@ public abstract class BasePlayer implements case DISCONTINUITY_REASON_SEEK_ADJUSTMENT: case DISCONTINUITY_REASON_INTERNAL: if (playQueue.getIndex() != newWindowIndex) { + resetPlaybackState(playQueue.getItem()); playQueue.setIndex(newWindowIndex); } break; @@ -750,6 +773,9 @@ public abstract class BasePlayer implements @Override public void onSeekProcessed() { if (DEBUG) Log.d(TAG, "ExoPlayer - onSeekProcessed() called"); + if (isPrepared) { + savePlaybackState(); + } } /*////////////////////////////////////////////////////////////////////////// // 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 (DEBUG) Log.d(TAG, "savePlaybackState() called"); final Disposable stateSaver = recordManager.saveStreamState(info, progress) .observeOn(AndroidSchedulers.mainThread()) + .doOnError((e) -> { + if (DEBUG) e.printStackTrace(); + }) .onErrorComplete() - .subscribe( - ignored -> {/* successful */}, - error -> Log.e(TAG, "savePlaybackState() failure: ", error) - ); + .subscribe(); 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; final StreamInfo currentInfo = currentMetadata.getMetadata(); - - if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD_MILLIS && - simpleExoPlayer.getCurrentPosition() < - simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD_MILLIS) { - savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition()); - } + savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition()); } private void maybeUpdateCurrentMetadata() { @@ -1225,4 +1264,10 @@ public abstract class BasePlayer implements public boolean gotDestroyed() { 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); + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index cf179917d..902ee5065 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -247,6 +247,12 @@ public final class MainVideoPlayer extends AppCompatActivity super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(newBase)); } + @Override + protected void onPause() { + playerImpl.savePlaybackState(); + super.onPause(); + } + /*////////////////////////////////////////////////////////////////////////// // State Saving //////////////////////////////////////////////////////////////////////////*/ @@ -579,7 +585,8 @@ public final class MainVideoPlayer extends AppCompatActivity this.getPlaybackSpeed(), this.getPlaybackPitch(), this.getPlaybackSkipSilence(), - this.getPlaybackQuality() + this.getPlaybackQuality(), + false ); context.startService(intent); @@ -601,7 +608,8 @@ public final class MainVideoPlayer extends AppCompatActivity this.getPlaybackSpeed(), this.getPlaybackPitch(), this.getPlaybackSkipSilence(), - this.getPlaybackQuality() + this.getPlaybackQuality(), + false ); context.startService(intent); diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index 7578c444c..3782d85c0 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -325,6 +325,7 @@ public final class PopupVideoPlayer extends Service { isPopupClosing = true; if (playerImpl != null) { + playerImpl.savePlaybackState(); if (playerImpl.getRootView() != null) { windowManager.removeView(playerImpl.getRootView()); } @@ -565,7 +566,8 @@ public final class PopupVideoPlayer extends Service { this.getPlaybackSpeed(), this.getPlaybackPitch(), this.getPlaybackSkipSilence(), - this.getPlaybackQuality() + this.getPlaybackQuality(), + false ); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 3e04f1e3a..bdd31f21b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -188,7 +188,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity this.player.getPlaybackSpeed(), this.player.getPlaybackPitch(), this.player.getPlaybackSkipSilence(), - null + null, + false ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index fb60ac473..bbfe805a5 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -543,6 +543,11 @@ public abstract class VideoPlayer extends BasePlayer playbackSpeedTextView.setText(formatSpeed(getPlaybackSpeed())); super.onPrepared(playWhenReady); + + if (simpleExoPlayer.getCurrentPosition() != 0 && !isControlsVisible()) { + controlsVisibilityHandler.removeCallbacksAndMessages(null); + controlsVisibilityHandler.postDelayed(this::showControlsThenHide, DEFAULT_CONTROLS_DURATION); + } } @Override diff --git a/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java b/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java index e1ecc662d..ac79fee23 100644 --- a/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java +++ b/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java @@ -124,7 +124,7 @@ public class CommentTextOnTouchListener implements View.OnTouchListener { .observeOn(AndroidSchedulers.mainThread()) .subscribe(info -> { PlayQueue playQueue = new SinglePlayQueue((StreamInfo) info, seconds*1000); - NavigationHelper.playOnPopupPlayer(context, playQueue); + NavigationHelper.playOnPopupPlayer(context, playQueue, false); }); return true; } diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 98ae3a88a..89c4b33fe 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -69,12 +69,14 @@ public class NavigationHelper { public static Intent getPlayerIntent(@NonNull final Context context, @NonNull final Class targetClazz, @NonNull final PlayQueue playQueue, - @Nullable final String quality) { + @Nullable final String quality, + final boolean resumePlayback) { Intent intent = new Intent(context, targetClazz); final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class); if (cacheKey != null) intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey); if (quality != null) intent.putExtra(VideoPlayer.PLAYBACK_QUALITY, quality); + intent.putExtra(VideoPlayer.RESUME_PLAYBACK, resumePlayback); return intent; } @@ -82,16 +84,18 @@ public class NavigationHelper { @NonNull public static Intent getPlayerIntent(@NonNull final Context context, @NonNull final Class targetClazz, - @NonNull final PlayQueue playQueue) { - return getPlayerIntent(context, targetClazz, playQueue, null); + @NonNull final PlayQueue playQueue, + final boolean resumePlayback) { + return getPlayerIntent(context, targetClazz, playQueue, null, resumePlayback); } @NonNull public static Intent getPlayerEnqueueIntent(@NonNull final Context context, @NonNull final Class targetClazz, @NonNull final PlayQueue playQueue, - final boolean selectOnAppend) { - return getPlayerIntent(context, targetClazz, playQueue) + final boolean selectOnAppend, + final boolean resumePlayback) { + return getPlayerIntent(context, targetClazz, playQueue, resumePlayback) .putExtra(BasePlayer.APPEND_ONLY, true) .putExtra(BasePlayer.SELECT_ON_APPEND, selectOnAppend); } @@ -104,40 +108,41 @@ public class NavigationHelper { final float playbackSpeed, final float playbackPitch, final boolean playbackSkipSilence, - @Nullable final String playbackQuality) { - return getPlayerIntent(context, targetClazz, playQueue, playbackQuality) + @Nullable final String playbackQuality, + final boolean resumePlayback) { + return getPlayerIntent(context, targetClazz, playQueue, playbackQuality, resumePlayback) .putExtra(BasePlayer.REPEAT_MODE, repeatMode) .putExtra(BasePlayer.PLAYBACK_SPEED, playbackSpeed) .putExtra(BasePlayer.PLAYBACK_PITCH, playbackPitch) .putExtra(BasePlayer.PLAYBACK_SKIP_SILENCE, playbackSkipSilence); } - public static void playOnMainPlayer(final Context context, final PlayQueue queue) { - final Intent playerIntent = getPlayerIntent(context, MainVideoPlayer.class, queue); + public static void playOnMainPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) { + final Intent playerIntent = getPlayerIntent(context, MainVideoPlayer.class, queue, resumePlayback); playerIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 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)) { PermissionHelper.showPopupEnablementToast(context); return; } 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(); - 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) { - enqueueOnPopupPlayer(context, queue, false); + public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) { + 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)) { PermissionHelper.showPopupEnablementToast(context); return; @@ -145,17 +150,17 @@ public class NavigationHelper { Toast.makeText(context, R.string.popup_playing_append, Toast.LENGTH_SHORT).show(); 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) { - enqueueOnBackgroundPlayer(context, queue, false); + public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) { + 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(); 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) { diff --git a/app/src/main/java/org/schabi/newpipe/views/AnimatedProgressBar.java b/app/src/main/java/org/schabi/newpipe/views/AnimatedProgressBar.java new file mode 100644 index 000000000..39c6e707e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/AnimatedProgressBar.java @@ -0,0 +1,69 @@ +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); + } + + @Override + public synchronized void setProgress(int progress) { + cancelAnimation(); + animation = new ProgressBarAnimation(this, getProgress(), progress); + startAnimation(animation); + } + + private void cancelAnimation() { + if (animation != null) { + animation.cancel(); + animation = null; + } + clearAnimation(); + } + + private void setProgressInternal(int progress) { + super.setProgress(progress); + } + + 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.setProgressInternal((int) value); + } + } +} diff --git a/app/src/main/res/drawable/progress_soundcloud_horizontal_dark.xml b/app/src/main/res/drawable/progress_soundcloud_horizontal_dark.xml new file mode 100644 index 000000000..1ec9f67b6 --- /dev/null +++ b/app/src/main/res/drawable/progress_soundcloud_horizontal_dark.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/progress_soundcloud_horizontal_light.xml b/app/src/main/res/drawable/progress_soundcloud_horizontal_light.xml new file mode 100644 index 000000000..c326c5c04 --- /dev/null +++ b/app/src/main/res/drawable/progress_soundcloud_horizontal_light.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/progress_youtube_horizontal_dark.xml b/app/src/main/res/drawable/progress_youtube_horizontal_dark.xml new file mode 100644 index 000000000..404410f98 --- /dev/null +++ b/app/src/main/res/drawable/progress_youtube_horizontal_dark.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/progress_youtube_horizontal_light.xml b/app/src/main/res/drawable/progress_youtube_horizontal_light.xml new file mode 100644 index 000000000..120a6e5fb --- /dev/null +++ b/app/src/main/res/drawable/progress_youtube_horizontal_light.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-large-land/fragment_video_detail.xml b/app/src/main/res/layout-large-land/fragment_video_detail.xml index 8cdc2f307..a8fbcca89 100644 --- a/app/src/main/res/layout-large-land/fragment_video_detail.xml +++ b/app/src/main/res/layout-large-land/fragment_video_detail.xml @@ -1,7 +1,7 @@ @@ -67,10 +68,10 @@ android:layout_height="wrap_content" android:layout_gravity="center" android:background="#64000000" - android:paddingBottom="10dp" android:paddingLeft="30dp" - android:paddingRight="30dp" android:paddingTop="10dp" + android:paddingRight="30dp" + android:paddingBottom="10dp" android:text="@string/hold_to_append" android:textColor="@android:color/white" android:textSize="20sp" @@ -84,17 +85,42 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|right" - android:layout_marginBottom="8dp" android:layout_marginLeft="12dp" - android:layout_marginRight="12dp" android:layout_marginTop="8dp" + android:layout_marginRight="12dp" + android:layout_marginBottom="8dp" android:alpha=".6" android:background="#23000000" android:gravity="center" - android:paddingBottom="2dp" android:paddingLeft="6dp" - android:paddingRight="6dp" android:paddingTop="2dp" + android:paddingRight="6dp" + android:paddingBottom="2dp" + android:textAllCaps="true" + android:textColor="@android:color/white" + android:textSize="12sp" + android:textStyle="bold" + android:visibility="gone" + tools:ignore="RtlHardcoded" + tools:text="12:38" + tools:visibility="visible" /> + + + + @@ -191,8 +230,8 @@ android:layout_width="match_parent" android:layout_height="55dp" android:layout_marginLeft="12dp" - android:layout_marginRight="12dp" android:layout_marginTop="6dp" + android:layout_marginRight="12dp" android:baselineAligned="false" android:orientation="horizontal"> @@ -201,8 +240,8 @@ android:id="@+id/detail_uploader_root_layout" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_toLeftOf="@id/details_panel" android:layout_toStartOf="@id/details_panel" + android:layout_toLeftOf="@id/details_panel" android:background="?attr/selectableItemBackground" android:gravity="center_vertical" android:orientation="horizontal" @@ -261,8 +300,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" - android:layout_marginBottom="6dp" android:layout_marginTop="6dp" + android:layout_marginBottom="6dp" android:lines="1" android:textAppearance="?android:attr/textAppearanceLarge" android:textSize="@dimen/video_item_detail_views_text_size" @@ -354,8 +393,8 @@ android:drawableTop="?attr/ic_playlist_add" android:focusable="true" android:gravity="center" - android:paddingBottom="6dp" android:paddingTop="6dp" + android:paddingBottom="6dp" android:text="@string/controls_add_to_playlist_title" android:textSize="12sp" /> @@ -371,8 +410,8 @@ android:drawableTop="?attr/audio" android:focusable="true" android:gravity="center" - android:paddingBottom="6dp" android:paddingTop="6dp" + android:paddingBottom="6dp" android:text="@string/controls_background_title" android:textSize="12sp" /> @@ -388,8 +427,8 @@ android:drawableTop="?attr/popup" android:focusable="true" android:gravity="center" - android:paddingBottom="6dp" android:paddingTop="6dp" + android:paddingBottom="6dp" android:text="@string/controls_popup_title" android:textSize="12sp" /> @@ -405,8 +444,8 @@ android:drawableTop="?attr/download" android:focusable="true" android:gravity="center" - android:paddingBottom="6dp" android:paddingTop="6dp" + android:paddingBottom="6dp" android:text="@string/download" android:textSize="12sp" /> @@ -444,10 +483,10 @@ android:id="@+id/detail_description_view" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginBottom="8dp" android:layout_marginLeft="12dp" - android:layout_marginRight="12dp" android:layout_marginTop="3dp" + android:layout_marginRight="12dp" + android:layout_marginBottom="8dp" android:textAppearance="?android:attr/textAppearanceMedium" android:textIsSelectable="true" android:textSize="@dimen/video_item_detail_description_text_size" @@ -490,7 +529,7 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/video_item_detail" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:focusableInTouchMode="true"> - + - + - + - - + + - + - + - + - - + - + - - + - - + - + - + + - + + - - + - - + - - + - - + + - - + + - + + - + + - - + + - - + - + - + + - + + - + - + - - - + - + - - + - + + + - + - + + - + - + - - + - + - + - + + - + - - + - + + + + + + + + - + - + - + - + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 32d57db8b..759c24460 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -167,7 +167,10 @@ Что нового История поиска Хранить запросы поиска локально - История и кэш + История просмотров + Продолжать воспроизведение + Восстанавливать с последней позиции + Очистить данные Запоминать воспроизведённые потоки Возобновить при фокусе Возобновлять воспроизведение после перерывов (например, телефонных звонков) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index eaca5719a..41e6f22ba 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -153,7 +153,10 @@ Показувати пропозиції під час пошуку Історія пошуків Зберігати пошукові запити локально - Історія та кеш + Історія переглядiв + Продовживати перегляд + Відновлювати останню позицію + Очистити дані Вести облік перегляду відеозаписів Відновити відтворення Продовжувати відтворення опісля переривання (наприклад телефонного дзвінка) diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 865b68c24..bdf42c4ed 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -43,6 +43,7 @@ + diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 3861f53d5..b70305d56 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -150,6 +150,7 @@ enable_search_history enable_watch_history main_page_content + enable_playback_resume import_data export_data diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 98a32d9e6..a84d7db36 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -95,7 +95,10 @@ Show suggestions when searching Search history Store search queries locally - History & Cache + Watch history + Resume playback + Restore last playback position + Clear data Keep track of watched videos Resume on focus gain Continue playing after interruptions (e.g. phone calls) diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index a7686dedc..6df9069ff 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -65,6 +65,7 @@ @drawable/toolbar_shadow_light @drawable/light_selector @color/light_ripple_color + @drawable/progress_youtube_horizontal_light @style/PreferenceThemeOverlay.v14.Material @@ -128,6 +129,7 @@ @drawable/toolbar_shadow_dark @drawable/dark_selector @color/dark_ripple_color + @drawable/progress_youtube_horizontal_dark @style/PreferenceThemeOverlay.v14.Material diff --git a/app/src/main/res/values/styles_services.xml b/app/src/main/res/values/styles_services.xml index 257b1905d..d6ab239e4 100644 --- a/app/src/main/res/values/styles_services.xml +++ b/app/src/main/res/values/styles_services.xml @@ -15,18 +15,21 @@ @color/light_soundcloud_primary_color @color/light_soundcloud_dark_color @color/light_soundcloud_accent_color + @drawable/progress_soundcloud_horizontal_light diff --git a/app/src/main/res/xml/history_settings.xml b/app/src/main/res/xml/history_settings.xml index a7428d340..305b1c360 100644 --- a/app/src/main/res/xml/history_settings.xml +++ b/app/src/main/res/xml/history_settings.xml @@ -1,40 +1,54 @@ - + android:title="@string/enable_watch_history_title" + app:iconSpaceReserved="false" /> + + + android:title="@string/enable_search_history_title" + app:iconSpaceReserved="false" /> - + - + - + - + + + + + \ No newline at end of file From 002a1412cbd122328ad14bcfd9d559a40de01de0 Mon Sep 17 00:00:00 2001 From: Vasiliy Date: Mon, 15 Apr 2019 21:22:31 +0300 Subject: [PATCH 02/30] Fix scrolling details --- app/src/main/res/layout-large-land/fragment_video_detail.xml | 1 + app/src/main/res/layout/fragment_video_detail.xml | 1 + 2 files changed, 2 insertions(+) diff --git a/app/src/main/res/layout-large-land/fragment_video_detail.xml b/app/src/main/res/layout-large-land/fragment_video_detail.xml index a8fbcca89..f773c69cf 100644 --- a/app/src/main/res/layout-large-land/fragment_video_detail.xml +++ b/app/src/main/res/layout-large-land/fragment_video_detail.xml @@ -142,6 +142,7 @@ android:background="@android:color/transparent" android:progressDrawable="?attr/progress_horizontal_drawable" android:visibility="invisible" + app:layout_scrollFlags="scroll" tools:max="100" tools:progress="40" tools:visibility="visible" /> diff --git a/app/src/main/res/layout/fragment_video_detail.xml b/app/src/main/res/layout/fragment_video_detail.xml index 1dfc61cf7..2139fd0cc 100644 --- a/app/src/main/res/layout/fragment_video_detail.xml +++ b/app/src/main/res/layout/fragment_video_detail.xml @@ -140,6 +140,7 @@ android:layout_marginTop="-2dp" android:progressDrawable="?attr/progress_horizontal_drawable" android:visibility="invisible" + app:layout_scrollFlags="scroll" tools:max="100" tools:progress="40" tools:visibility="visible" /> From 73be8cf074cf164b478d5785dc5c1e40ac14e999 Mon Sep 17 00:00:00 2001 From: Vasiliy Date: Mon, 15 Apr 2019 21:19:59 +0300 Subject: [PATCH 03/30] Base implementation of showing playback positions in lists --- .../fragments/list/BaseListFragment.java | 6 +++ .../newpipe/info_list/InfoItemBuilder.java | 11 +++-- .../newpipe/info_list/InfoListAdapter.java | 44 +++++++++++++++++-- .../holder/ChannelInfoItemHolder.java | 6 ++- .../holder/ChannelMiniInfoItemHolder.java | 4 +- .../holder/CommentsInfoItemHolder.java | 8 ++-- .../holder/CommentsMiniInfoItemHolder.java | 4 +- .../info_list/holder/InfoItemHolder.java | 4 +- .../holder/PlaylistMiniInfoItemHolder.java | 4 +- .../holder/StreamInfoItemHolder.java | 6 ++- .../holder/StreamMiniInfoItemHolder.java | 18 +++++++- .../local/history/HistoryRecordManager.java | 35 +++++++++++++++ .../subscription/SubscriptionFragment.java | 9 ++-- .../main/res/layout/list_stream_grid_item.xml | 12 +++++ app/src/main/res/layout/list_stream_item.xml | 12 +++++ .../main/res/layout/list_stream_mini_item.xml | 12 +++++ 16 files changed, 172 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index 68784852e..5a49cce28 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -65,6 +65,12 @@ public abstract class BaseListFragment extends BaseStateFragment implem infoListAdapter = new InfoListAdapter(activity); } + @Override + public void onDetach() { + infoListAdapter.dispose(); + super.onDetach(); + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java index 0e9fd3277..39f7971dd 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java @@ -2,12 +2,14 @@ package org.schabi.newpipe.info_list; import android.content.Context; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; import android.view.View; import android.view.ViewGroup; 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.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.comments.CommentsInfoItem; @@ -59,13 +61,14 @@ public class InfoItemBuilder { this.context = context; } - public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem) { - return buildView(parent, infoItem, false); + public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, @Nullable StreamStateEntity state) { + 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); - holder.updateFromItem(infoItem); + holder.updateFromItem(infoItem, state); return holder.itemView; } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index 5e7095c7d..8e54f582a 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -7,16 +7,17 @@ import android.util.Log; import android.view.View; import android.view.ViewGroup; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; 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.playlist.PlaylistInfoItem; 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.ChannelMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder; 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.PlaylistGridInfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder; @@ -24,12 +25,16 @@ import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamGridInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder; +import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.util.FallbackViewHolder; import org.schabi.newpipe.util.OnClickGesture; import java.util.ArrayList; import java.util.List; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; + /* * Created by Christian Schabesberger on 01.08.16. * @@ -70,7 +75,10 @@ public class InfoListAdapter extends RecyclerView.Adapter infoItemList; + private final ArrayList states; + private final CompositeDisposable stateLoaders; private boolean useMiniVariant = false; private boolean useGridVariant = false; private boolean showFooter = false; @@ -88,7 +96,10 @@ public class InfoListAdapter extends RecyclerView.Adapter(); + states = new ArrayList<>(); + stateLoaders = new CompositeDisposable(); } public void setOnStreamSelectedListener(OnClickGesture listener) { @@ -115,7 +126,17 @@ public class InfoListAdapter extends RecyclerView.Adapter data) { + public void addInfoItemList(final List data) { + stateLoaders.add( + historyRecordManager.loadStreamStateBatch(data) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(streamStateEntities -> { + addInfoItemList(data, streamStateEntities); + }) + ); + } + + private void addInfoItemList(List data, List statesEntities) { if (data != null) { if (DEBUG) { Log.d(TAG, "addInfoItemList() before > infoItemList.size() = " + infoItemList.size() + ", data.size() = " + data.size()); @@ -123,6 +144,7 @@ public class InfoListAdapter extends RecyclerView.Adapter offsetStart = " + offsetStart + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter); @@ -140,6 +162,16 @@ public class InfoListAdapter extends RecyclerView.Adapter { + addInfoItem(data, streamStateEntity); + }) + ); + } + + private void addInfoItem(InfoItem data, StreamStateEntity state) { if (data != null) { if (DEBUG) { Log.d(TAG, "addInfoItem() before > infoItemList.size() = " + infoItemList.size() + ", thread = " + Thread.currentThread()); @@ -147,6 +179,7 @@ public class InfoListAdapter extends RecyclerView.Adapter position = " + positionInserted + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter); @@ -167,6 +200,7 @@ public class InfoListAdapter extends RecyclerView.Adapter loadStreamState(final InfoItem info) { + return Single.fromCallable(() -> { + final List entities = streamTable.getStream(info.getServiceId(), info.getUrl()).blockingFirst(); + if (entities.isEmpty()) { + return null; + } + final List states = streamStateTable.getState(entities.get(0).getUid()).blockingFirst(); + if (states.isEmpty()) { + return null; + } + return states.get(0); + }).subscribeOn(Schedulers.io()); + } + + public Single> loadStreamStateBatch(final List infos) { + return Single.fromCallable(() -> { + final List result = new ArrayList<>(infos.size()); + for (InfoItem info : infos) { + final List entities = streamTable.getStream(info.getServiceId(), info.getUrl()).blockingFirst(); + if (entities.isEmpty()) { + result.add(null); + continue; + } + final List 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()); + } + /////////////////////////////////////////////////////// // Utility /////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java index 7d0fc3e61..364d50df6 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java @@ -23,7 +23,6 @@ import android.support.annotation.Nullable; import android.support.v4.app.FragmentManager; import android.support.v4.content.LocalBroadcastManager; import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.LinearLayoutManager; 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.fragments.BaseStateFragment; 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.SubscriptionsImportService; -import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.NavigationHelper; @@ -131,6 +128,12 @@ public class SubscriptionFragment extends BaseStateFragment + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_stream_item.xml b/app/src/main/res/layout/list_stream_item.xml index db4af7f8c..ccf325592 100644 --- a/app/src/main/res/layout/list_stream_item.xml +++ b/app/src/main/res/layout/list_stream_item.xml @@ -79,4 +79,16 @@ android:textAppearance="?android:attr/textAppearanceSmall" android:textSize="@dimen/video_item_search_upload_date_text_size" tools:text="2 years ago • 10M views"/> + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_stream_mini_item.xml b/app/src/main/res/layout/list_stream_mini_item.xml index bffd13a6f..383580e59 100644 --- a/app/src/main/res/layout/list_stream_mini_item.xml +++ b/app/src/main/res/layout/list_stream_mini_item.xml @@ -69,4 +69,16 @@ android:textAppearance="?android:attr/textAppearanceSmall" android:textSize="@dimen/video_item_search_uploader_text_size" tools:text="Uploader" /> + + + \ No newline at end of file From a48cbc697151ba4b2aeb20723e1afd7ccc74dc14 Mon Sep 17 00:00:00 2001 From: Vasiliy Date: Mon, 15 Apr 2019 22:18:24 +0300 Subject: [PATCH 04/30] Show streams states for local lists --- .../newpipe/info_list/InfoListAdapter.java | 2 +- .../newpipe/local/BaseLocalListFragment.java | 1 + .../newpipe/local/LocalItemListAdapter.java | 28 +++++++++++++- .../local/dialog/PlaylistAppendDialog.java | 5 ++- .../local/history/HistoryRecordManager.java | 37 +++++++++++++++++-- .../newpipe/local/holder/LocalItemHolder.java | 4 +- .../local/holder/LocalPlaylistItemHolder.java | 6 ++- .../holder/LocalPlaylistStreamItemHolder.java | 15 +++++++- .../LocalStatisticStreamItemHolder.java | 15 +++++++- .../local/holder/PlaylistItemHolder.java | 4 +- .../holder/RemotePlaylistItemHolder.java | 6 ++- .../layout/list_stream_playlist_grid_item.xml | 11 ++++++ .../res/layout/list_stream_playlist_item.xml | 11 ++++++ app/src/main/res/values-land/dimens.xml | 2 +- app/src/main/res/values/dimens.xml | 2 +- 15 files changed, 132 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index 8e54f582a..df207abb5 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -166,7 +166,7 @@ public class InfoListAdapter extends RecyclerView.Adapter { - addInfoItem(data, streamStateEntity); + addInfoItem(data, streamStateEntity[0]); }) ); } diff --git a/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java b/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java index abdf82353..20676c6db 100644 --- a/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java @@ -150,6 +150,7 @@ public abstract class BaseLocalListFragment extends BaseStateFragment public void onDestroyView() { super.onDestroyView(); itemsList = null; + itemListAdapter.dispose(); itemListAdapter = null; } diff --git a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java index e298dedd3..372392d7b 100644 --- a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java @@ -8,6 +8,8 @@ import android.view.View; import android.view.ViewGroup; import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; +import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.holder.LocalItemHolder; import org.schabi.newpipe.local.holder.LocalPlaylistGridItemHolder; import org.schabi.newpipe.local.holder.LocalPlaylistItemHolder; @@ -25,6 +27,9 @@ import java.text.DateFormat; import java.util.ArrayList; import java.util.List; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; + /* * Created by Christian Schabesberger on 01.08.16. * @@ -63,7 +68,10 @@ public class LocalItemListAdapter extends RecyclerView.Adapter localItems; + private final ArrayList states; + private final CompositeDisposable stateLoaders; private final DateFormat dateFormat; private boolean showFooter = false; @@ -73,9 +81,12 @@ public class LocalItemListAdapter extends RecyclerView.Adapter(); dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Localization.getPreferredLocale(activity)); + states = new ArrayList<>(); + stateLoaders = new CompositeDisposable(); } public void setSelectedListener(OnClickGesture listener) { @@ -87,6 +98,15 @@ public class LocalItemListAdapter extends RecyclerView.Adapter data) { + stateLoaders.add( + historyRecordManager.loadLocalStreamStateBatch(data) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(streamStateEntities -> + addItems(data, streamStateEntities)) + ); + } + + private void addItems(List data, List streamStates) { if (data != null) { if (DEBUG) { Log.d(TAG, "addItems() before > localItems.size() = " + @@ -95,6 +115,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter offsetStart = " + offsetStart + @@ -130,6 +151,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter= localItems.size() || actualTo >= localItems.size()) return false; localItems.add(actualTo, localItems.remove(actualFrom)); + states.add(actualTo, states.remove(actualFrom)); notifyItemMoved(fromAdapterPosition, toAdapterPosition); return true; } @@ -259,7 +281,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter loadStreamState(final InfoItem info) { + public Single loadStreamState(final InfoItem info) { return Single.fromCallable(() -> { final List entities = streamTable.getStream(info.getServiceId(), info.getUrl()).blockingFirst(); if (entities.isEmpty()) { - return null; + return new StreamStateEntity[]{null}; } final List states = streamStateTable.getState(entities.get(0).getUid()).blockingFirst(); if (states.isEmpty()) { - return null; + return new StreamStateEntity[]{null}; } - return states.get(0); + return new StreamStateEntity[]{states.get(0)}; }).subscribeOn(Schedulers.io()); } @@ -255,6 +258,32 @@ public class HistoryRecordManager { }).subscribeOn(Schedulers.io()); } + public Single> loadLocalStreamStateBatch(final List items) { + return Single.fromCallable(() -> { + final List 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 states = streamStateTable.getState(streamId).blockingFirst(); + if (states.isEmpty()) { + result.add(null); + continue; + } + result.add(states.get(0)); + } + return result; + }).subscribeOn(Schedulers.io()); + } + /////////////////////////////////////////////////////// // Utility /////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalItemHolder.java index 889751afa..01af60f98 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalItemHolder.java @@ -1,10 +1,12 @@ package org.schabi.newpipe.local.holder; +import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.ViewGroup; import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.local.LocalItemBuilder; import java.text.DateFormat; @@ -38,5 +40,5 @@ public abstract class LocalItemHolder extends RecyclerView.ViewHolder { 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); } diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java index 8743684ee..0e6eca9ba 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java @@ -1,10 +1,12 @@ package org.schabi.newpipe.local.holder; +import android.support.annotation.Nullable; import android.view.View; import android.view.ViewGroup; import org.schabi.newpipe.database.LocalItem; 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.util.ImageDisplayConstants; @@ -21,7 +23,7 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder { } @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; final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem; @@ -32,6 +34,6 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder { itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS); - super.updateFromItem(localItem, dateFormat); + super.updateFromItem(localItem, state, dateFormat); } } diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java index e591b73e5..48bbbc81d 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java @@ -1,21 +1,25 @@ package org.schabi.newpipe.local.holder; +import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; +import android.widget.ProgressBar; import android.widget.TextView; import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; 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.local.LocalItemBuilder; import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; import java.text.DateFormat; +import java.util.concurrent.TimeUnit; public class LocalPlaylistStreamItemHolder extends LocalItemHolder { @@ -24,6 +28,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { public final TextView itemAdditionalDetailsView; public final TextView itemDurationView; public final View itemHandleView; + public final ProgressBar itemProgressView; LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { super(infoItemBuilder, layoutId, parent); @@ -33,6 +38,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { itemAdditionalDetailsView = itemView.findViewById(R.id.itemAdditionalDetails); itemDurationView = itemView.findViewById(R.id.itemDurationView); itemHandleView = itemView.findViewById(R.id.itemHandle); + itemProgressView = itemView.findViewById(R.id.itemProgressView); } public LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { @@ -40,7 +46,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { } @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; final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem; @@ -53,6 +59,13 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.duration_background_color)); 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 { itemDurationView.setVisibility(View.GONE); } diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java index 57a5794e3..ac194b776 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java @@ -5,17 +5,20 @@ import android.support.v4.content.ContextCompat; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; +import android.widget.ProgressBar; import android.widget.TextView; import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; 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.local.LocalItemBuilder; import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; import java.text.DateFormat; +import java.util.concurrent.TimeUnit; /* * Created by Christian Schabesberger on 01.08.16. @@ -45,6 +48,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { public final TextView itemDurationView; @Nullable public final TextView itemAdditionalDetails; + public final ProgressBar itemProgressView; public LocalStatisticStreamItemHolder(LocalItemBuilder itemBuilder, ViewGroup parent) { this(itemBuilder, R.layout.list_stream_item, parent); @@ -58,6 +62,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { itemUploaderView = itemView.findViewById(R.id.itemUploaderView); itemDurationView = itemView.findViewById(R.id.itemDurationView); itemAdditionalDetails = itemView.findViewById(R.id.itemAdditionalDetails); + itemProgressView = itemView.findViewById(R.id.itemProgressView); } private String getStreamInfoDetailLine(final StreamStatisticsEntry entry, @@ -70,7 +75,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { } @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; final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem; @@ -82,8 +87,16 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.duration_background_color)); 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 { itemDurationView.setVisibility(View.GONE); + itemProgressView.setVisibility(View.GONE); } if (itemAdditionalDetails != null) { diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/PlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/PlaylistItemHolder.java index 5d6f192e1..2a81f9571 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/PlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/PlaylistItemHolder.java @@ -1,11 +1,13 @@ package org.schabi.newpipe.local.holder; +import android.support.annotation.Nullable; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.local.LocalItemBuilder; import java.text.DateFormat; @@ -31,7 +33,7 @@ public abstract class PlaylistItemHolder extends LocalItemHolder { } @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 -> { if (itemBuilder.getOnItemSelectedListener() != null) { itemBuilder.getOnItemSelectedListener().selected(localItem); diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java index 5b2a88d38..bdcd42f67 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java @@ -1,9 +1,11 @@ package org.schabi.newpipe.local.holder; +import android.support.annotation.Nullable; import android.view.ViewGroup; import org.schabi.newpipe.database.LocalItem; 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.local.LocalItemBuilder; import org.schabi.newpipe.util.ImageDisplayConstants; @@ -21,7 +23,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder { } @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; final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem; @@ -33,6 +35,6 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder { itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView, ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS); - super.updateFromItem(localItem, dateFormat); + super.updateFromItem(localItem, state, dateFormat); } } diff --git a/app/src/main/res/layout/list_stream_playlist_grid_item.xml b/app/src/main/res/layout/list_stream_playlist_grid_item.xml index 4b31a452e..ea680c072 100644 --- a/app/src/main/res/layout/list_stream_playlist_grid_item.xml +++ b/app/src/main/res/layout/list_stream_playlist_grid_item.xml @@ -81,4 +81,15 @@ android:textSize="@dimen/video_item_search_uploader_text_size" tools:text="Uploader" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_stream_playlist_item.xml b/app/src/main/res/layout/list_stream_playlist_item.xml index 193b3fea4..47c3ab93d 100644 --- a/app/src/main/res/layout/list_stream_playlist_item.xml +++ b/app/src/main/res/layout/list_stream_playlist_item.xml @@ -83,4 +83,15 @@ android:textSize="@dimen/video_item_search_uploader_text_size" tools:text="Uploader" /> + + \ No newline at end of file diff --git a/app/src/main/res/values-land/dimens.xml b/app/src/main/res/values-land/dimens.xml index ee047f0da..eea6c5cf0 100644 --- a/app/src/main/res/values-land/dimens.xml +++ b/app/src/main/res/values-land/dimens.xml @@ -14,7 +14,7 @@ 142dp 80dp - 104dp + 106dp 10dp 1sp diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 94101cfb0..582c4bade 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -15,7 +15,7 @@ 164dp 92dp - 94dp + 96dp 12dp 6dp From 1edfa78a055ba4adeaa73486f9e1354a869e1ab6 Mon Sep 17 00:00:00 2001 From: sherlock Date: Wed, 17 Apr 2019 16:45:40 +0530 Subject: [PATCH 05/30] removed the gena strings. --- .../fragments/detail/VideoDetailFragment.java | 13 +- .../schabi/newpipe/util/ExtractorHelper.java | 2 - app/src/main/res/values-ar/strings.xml | 1 - app/src/main/res/values-b+ast/strings.xml | 1 - app/src/main/res/values-be/strings.xml | 1 - app/src/main/res/values-bg/strings.xml | 1 - app/src/main/res/values-bn-rBD/strings.xml | 1 - app/src/main/res/values-ca/strings.xml | 1 - app/src/main/res/values-cmn/strings.xml | 1 - app/src/main/res/values-cs/strings.xml | 1 - app/src/main/res/values-da/strings.xml | 1 - app/src/main/res/values-de/strings.xml | 1 - app/src/main/res/values-el/strings.xml | 1 - app/src/main/res/values-eo/strings.xml | 1 - app/src/main/res/values-es/strings.xml | 1 - app/src/main/res/values-et/strings.xml | 1 - app/src/main/res/values-eu/strings.xml | 1 - app/src/main/res/values-fa/strings.xml | 1 - app/src/main/res/values-fi/strings.xml | 1 - app/src/main/res/values-fr/strings.xml | 1 - app/src/main/res/values-gl/strings.xml | 1 - app/src/main/res/values-he/strings.xml | 1 - app/src/main/res/values-hi/strings.xml | 1 - app/src/main/res/values-hr/strings.xml | 1 - app/src/main/res/values-hu/strings.xml | 1 - app/src/main/res/values-id/strings.xml | 1 - app/src/main/res/values-it/strings.xml | 1 - app/src/main/res/values-ja/strings.xml | 1 - app/src/main/res/values-ko/strings.xml | 1 - app/src/main/res/values-lt/strings.xml | 1 - app/src/main/res/values-mk/strings.xml | 1 - app/src/main/res/values-ms/strings.xml | 1 - app/src/main/res/values-nb-rNO/strings.xml | 1 - app/src/main/res/values-nl-rBE/strings.xml | 1 - app/src/main/res/values-nl/strings.xml | 1 - app/src/main/res/values-pl/strings.xml | 1 - app/src/main/res/values-pt-rBR/strings.xml | 1 - app/src/main/res/values-pt/strings.xml | 1 - app/src/main/res/values-ro/strings.xml | 1 - app/src/main/res/values-ru/strings.xml | 1 - app/src/main/res/values-sk/strings.xml | 1 - app/src/main/res/values-sl/strings.xml | 1 - app/src/main/res/values-sr/strings.xml | 1 - app/src/main/res/values-sv/strings.xml | 1 - app/src/main/res/values-tr/strings.xml | 1 - app/src/main/res/values-uk/strings.xml | 1 - app/src/main/res/values-vi/strings.xml | 1 - app/src/main/res/values-zh-rCN/strings.xml | 1 - app/src/main/res/values-zh-rHK/strings.xml | 1 - app/src/main/res/values-zh-rTW/strings.xml | 1 - app/src/main/res/values/strings.xml | 2 +- assets/gruese_die_gema.svg | 200 ------------------ 52 files changed, 2 insertions(+), 263 deletions(-) delete mode 100644 assets/gruese_die_gema.svg diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index bbd1a315d..81cecb49e 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -1220,9 +1220,7 @@ public class VideoDetailFragment protected boolean onError(Throwable exception) { if (super.onError(exception)) return true; - if (exception instanceof YoutubeStreamExtractor.GemaException) { - onBlockedByGemaError(); - } else if (exception instanceof ContentNotAvailableException) { + else if (exception instanceof ContentNotAvailableException) { showError(getString(R.string.content_not_available), false); } else { int errorId = exception instanceof YoutubeStreamExtractor.DecryptException @@ -1240,14 +1238,5 @@ public class VideoDetailFragment return true; } - public void onBlockedByGemaError() { - thumbnailBackgroundButton.setOnClickListener((View v) -> { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_VIEW); - intent.setData(Uri.parse(getString(R.string.c3s_url))); - startActivity(intent); - }); - showError(getString(R.string.blocked_by_gema), false, R.drawable.gruese_die_gema); - } } diff --git a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java index 0f1c39473..b70e5fdef 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -243,8 +243,6 @@ public final class ExtractorHelper { context.startActivity(intent); } else if (exception instanceof IOException) { Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show(); - } else if (exception instanceof YoutubeStreamExtractor.GemaException) { - Toast.makeText(context, R.string.blocked_by_gema, Toast.LENGTH_LONG).show(); } else if (exception instanceof ContentNotAvailableException) { Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show(); } else { diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 8493569a7..ca8c985d2 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -52,7 +52,6 @@ (إختبارية) إجراء التنزيلات من خلال استخدام بروكسي Tor لزيادة الخصوصية ( تشغيل الفيديو المباشر غير مدعوم حتى الأن ). استخدام تور مشاهدات %1$s - تم حجبه بواسطة GEMA محتوى غير متوفر تعذرت عملية تحميل كافة صور المعاينة خطأ diff --git a/app/src/main/res/values-b+ast/strings.xml b/app/src/main/res/values-b+ast/strings.xml index f9834119d..7f6402d4d 100644 --- a/app/src/main/res/values-b+ast/strings.xml +++ b/app/src/main/res/values-b+ast/strings.xml @@ -62,7 +62,6 @@ Nun pudo analizase\'l sitiu web Nun pudo analizase dafechu\'l sitiu web Conteníu non disponible - Bloquiáu por GEMA Nun pudo configurase\'l menú de descarga Esto ye una tresmisión de direuto qu\'entá nun se sofita. Nun pudo consiguise tresmisión dala diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 40c277884..29811116a 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -166,7 +166,6 @@ Не атрымалася разабраць вэб-сайт Не атрымалася цалкам разабраць вэб-сайт Кантэнт недаступны - Заблакавана GEMA Не атрымалася стварыць меню загрузкі Гэта прамая трансляцыя, яны пакуль не падтрымліваюцца. Не атрымалася знайсці ні аднаго патока diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index ce175b28e..8ac8eeec2 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -119,7 +119,6 @@ Неуспешно пресъздаване на уебсайта Не мога да пресъздам изцяло уебсайта Съдържанието не е налично - Блокирано от „GEMA“ Не мога да настроя меню за сваляне Предаването на живо все още не се поддържа Не мога да достъпя нито един поток diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index 3fb51af36..ad8bf5c71 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -92,7 +92,6 @@ ওয়েবসাইট বিশ্লেষন করা যায়নি। ওয়েবসাইট সম্পুর্নভাবে বিশ্লেষন করা যায়নি। কন্টেন্ট উপলব্ধ নয়। - GEMA কর্তৃক ব্লক করা হয়েছে। ডাউনলোড মেনু সেটআপ করা যায়নি। এটি একটি লাইভ স্ট্রিম। যা এখনও সমর্থিত নয়। কোনও স্ট্রিম পাওয়া যায়নি। diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 78608c2a5..f224cc8b4 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -192,7 +192,6 @@ No s\'ha pogut processar el lloc web No s\'ha pogut processar del tot el lloc web Contingut no disponible - Blocat per la GEMA No s\'ha pogut configurar el menú de baixades Les emissions en directe encara no són compatibles No s\'ha pogut obtenir cap vídeo diff --git a/app/src/main/res/values-cmn/strings.xml b/app/src/main/res/values-cmn/strings.xml index 897365ca9..168b67ae5 100644 --- a/app/src/main/res/values-cmn/strings.xml +++ b/app/src/main/res/values-cmn/strings.xml @@ -212,7 +212,6 @@ 无法解析网站 无法完全解析网站 内容不可用 - 内容被 GEMA 封锁 无法设置下载菜单 目前还不支持观看直播 无法获得任何媒体 diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 1e17f0621..0d8da54a2 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -52,7 +52,6 @@ Nebylo možné dekódovat URL videa Nebylo možné analyzovat stránku Obsah není k dispozici - Obsah blokuje GEMA Náhled videa Náhled videa diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index bd1e27af9..87284f203 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -175,7 +175,6 @@ Kunne ikke indlæse webside Kunne ikke indlæse hele websiden Indhold utilgængeligt - Blokeret af GEMA Kunne ikke oprette download menu "Livestreams er ikke undestøttet endnu " Kunne ikke hente nogen streams diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index e233e017b..c787d8e86 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -61,7 +61,6 @@ Konnte Video-URL-Signatur nicht entschlüsseln Konnte Webseite nicht analysieren Inhalt nicht verfügbar - Durch die GEMA gesperrt Inhalt Altersbeschränkte Inhalte diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 1106baf09..8448510f6 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -204,7 +204,6 @@ Δεν ήταν δυνατή η ανάλυση του ιστοτόπου Δεν ήταν δυνατή η ανάλυση ολόκληρου του ιστοτόπου Το περιεχόμενο δεν είναι διαθέσιμο - "Έχει αποκλειστεί από την GEMA" Δεν ήταν δυνατή η ρύθμιση του μενού λήψεων Η Ζωντανή Ροή δεν υποστηρίζεται ακόμα Δεν ήταν δυνατή η λήψη καμίας ροής diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 9ac81757f..668708130 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -37,7 +37,6 @@ Eraro Reteraro Enhavo ne estas disponebla - Blokita de GEMA Ŝatoj Malŝatoj diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 60257c508..a870445f1 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -46,7 +46,6 @@ Los audios descargados se almacenan aquí Introducir ruta de descarga para archivos de audio - Bloqueado por GEMA Carpeta de descarga de audio Vídeo y audio diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index bf5cee76b..46574391f 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -157,7 +157,6 @@ Veebilehe töötlemine nurjus Veebilehe täielik töötlemine nurjus Sisu pole saadaval - GEMA poolt blokeeritud Allalaadimismenüü seadistamine nurjus Otsevood ei ole veel toetatud Ühtegi voogu ei leitud diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index f6c723bd4..2b8f60f84 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -100,7 +100,6 @@ Ezin izan da webgunea analizatu Ezin izan da webgunea guztiz analizatu Edukia ez dago eskuragarri - GEMAk blokeatuta Ezin izan da deskargen menua ezarri Zuzeneko jarioek ez dute euskarririk oraindik Ezin izan da jariorik eskuratu diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 83a076f09..16ac060ab 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -66,7 +66,6 @@ نمی‌توان پایگاه وب را تجزیه کرد. نمی‌توان پایگاه وب را به صورت کامل تجزیه کرد. محتوا در دسترس نیست. - مسدود شده توسّط GEMA. نمی‌توان فهرست بارگیری را برپا ساخت. این یک جریان زنده است. این جریان‌ها هنوز پشتیبانی نمی‌شوند. نمی‌توان هیچ جریانی را گرفت. diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index ead13fb95..9a51969d4 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -114,7 +114,6 @@ Ei pystytty jäsentämään websivua Ei pystytty jäsentämään websivua kokonaan Sisältö ei ole saatavilla - Estetty GEMA Ei pystytty asettamaan latausvalikkoa Tämä on LIVE LÄHETYS, mitä ei vielä tueta. Ei saatu mitään suoratoistoa diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 98ed64f37..82d482952 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -60,7 +60,6 @@ Erreur Impossible d\'analyser le site web Contenu non disponible - Bloqué par GEMA Désolé, des erreurs se sont produites. Contenu diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index f137a092e..6922e4e2a 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -167,7 +167,6 @@ Non foi posíbel procesar o sitio web por completo O contido non está dispoñíbel \n - Bloqueado pola GEMA Non foi posíbel configurar o menú de descargas Isto é unha emisión en directo, polo que aínda non está soportado. Non foi posíbel obter unha emisión diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 5782a1b52..53d0e3b1a 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -91,7 +91,6 @@ ניתוח האתר לא התאפשר לא הייתה אפשרות לנתח את האתר לחלוטין תוכן אינו זמין - נחסם ע״י GEMA לא הייתה אפשרות להכין את תפריט ההורדה עדיין אין תמיכה בתזרימים חיים לא הייתה אפשרות לקבל תזרים כלשהו diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index d78e37403..de6ec3d4a 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -150,7 +150,6 @@ इस website का निरंक्षण नहीं कर सकते website का पूरी तरह से निरंक्षण नहीं हो सकता विषय वस्तु उपलब्ध नहीं है - GEMA ने block किया है डाउनलोड मेनू को स्थापित नहीं कर सकते "सीधे प्रसारण के लिये फिलहाल समर्थन नहीं है " कोई भी विडियो नहीं मिल रहा diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index d6972a210..29feb1e79 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -110,7 +110,6 @@ Nije moguće dohvatiti stranicu Nije moguće u potpunosti dohvatiti stranicu Sadržaj nije dostupan - Blokirano od GEMA-e Nije moguće postaviti izbornik za preuzimanje Ovo je PRIJENOS UŽIVO, koji još nije podržan. Nije moguće dobaviti stream diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 24662feed..52848ab0c 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -63,7 +63,6 @@ Korhatáros tartalom Hiba A tartalom nem elérhető - GEMA által blokkolt Ez egy élő közvetítés, amely még nem támogatott. Automatikus lejátszás Videók automatikus lejátszása, ha a NewPipe egy másik alkalmazásból lett indítva diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 9e8412e02..53061e63a 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -63,7 +63,6 @@ Tidak bisa mengurai situs web Tidak dapat menguraikan situs web sepenuhnya Konten tidak tersedia - Diblokir oleh GEMA Tidak bisa menyiapkan menu unduh Siaran langsung belum didukung Tidak bisa memuat gambar diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index b8cebdacb..105dad64a 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -59,7 +59,6 @@ Impossibile caricare tutte le miniature Impossibile decriptare la firma dell\'URL del video Contenuto non disponibile - Bloccato da GEMA Usa Tor (Sperimentale) Forza il download tramite Tor per una maggiore riservatezza (lo streaming dei video non è ancora supportato). diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index c53c401a0..04e1fcb04 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -62,7 +62,6 @@ 動画のURLを復号できませんでした Webサイトを解析できませんでした コンテンツが利用できません - GEMA にブロックされました 保存メニューを設定できませんでした diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index bcaafbcfc..adfed5e10 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -72,7 +72,6 @@ 웹사이트를 가져올 수 없습니다 웹사이트를 완전히 가져올 수 없습니다 컨텐츠를 사용할 수 없습니다 - GEMA에 의해 차단되었습니다 다운로드 메뉴를 설정할 수 없습니다 라이브 스트림은 아직 지원하지 않습니다 어떠한 스트림도 가져올 수 없습니다 diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 4ffcd26d6..52f8256c1 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -88,7 +88,6 @@ Negalima apdoroti tinklapio Negalima visiškai apdoroti tinklapio Turinys neprieinamas - Užblokavo GEMA Negalima sutvarkyti atsisiuntimų meniu Tai gyvas srautas. Tokie kol kas nepalaikomi. Negalima gauti jokio srauto diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 47413c6c3..6e4bdfc9b 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -152,7 +152,6 @@ Не може да се прочита страната Не може целосно да се прочита страната Содржината е недостапна - Блокирано од GEMA Неуспешно поставување на менито за превземања Ова е пренос ВО ЖИВО, што сеуште не е поддржано. Не е пронајден поток на податоци diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index 7c8b56bec..9810826a7 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -172,7 +172,6 @@ Tidak dapat menghuraikan laman web Tidak dapat menghuraikan laman web sepenuhnya Kandungan tidak tersedia - Disekat oleh GEMA Tidak dapat menyediakan menu muat turun Siaran langsung belum disokong Tidak boleh mendapat sebarang strim diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index baaa48ca8..5ef0df25c 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -70,7 +70,6 @@ Kunne ikke tolke nettside Kunne ikke tolke nettside fullstendig Innholdet er ikke tilgjengelig - Blokkert av GEMA Kunne ikke sette opp nedlastingsmeny Direktesendinger støttes ikke enda. Kunne ikke finne noen strømmer diff --git a/app/src/main/res/values-nl-rBE/strings.xml b/app/src/main/res/values-nl-rBE/strings.xml index 6eff6b040..48fe21bf3 100644 --- a/app/src/main/res/values-nl-rBE/strings.xml +++ b/app/src/main/res/values-nl-rBE/strings.xml @@ -143,7 +143,6 @@ Kon website niet verwerken Kon de website niet volledig inlezen Inhoud niet beschikbaar - Geblokkeerd door GEMA Kon downloadmenu niet instellen Livestreams worden nog niet ondersteund Kon geen streams vinden diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index db3eb00df..0c583a93b 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -55,7 +55,6 @@ Kan video-URL-ondertekening niet ontsleutelen Kan website niet verwerken Inhoud niet beschikbaar - Geblokkeerd door GEMA Kan downloadmenu niet instellen Livestreams zijn nog niet ondersteund Vind-ik-leuks diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index e1c03d75b..6ae382c8a 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -59,7 +59,6 @@ Nie można przetworzyć strony Nie można przetworzyć całości strony Zawartość niedostępna - Zablokowane przez GEMA Nie można ustawić menu pobierania Transmisje na żywo nie są jeszcze obsługiwane Nie można otrzymać żadnego strumienia diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 2c73758b1..a0f82e3f2 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -12,7 +12,6 @@ Reproduz o vídeo quando o NewPipe for aberto a partir de outro aplicativo Reproduzir automaticamente Preto - Bloqueado pelo GEMA Cancelar Soma de Verificação Escolher navegador diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 99156d49f..8226d1e79 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -61,7 +61,6 @@ Incapaz de descodificar a assinatura do vídeo Incapaz de processar o site Conteúdo não disponível - Bloqueado por GEMA Conteúdo Restringir conteúdo por idade diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 8e9f83fdf..31451b27c 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -57,7 +57,6 @@ Nu s-a putut decripta semnătura URL a videoclipului Nu s-a putut analiza website-ul Conținut indisponibil - Blocat de către GEMA Imposibil de inițializat meniul pentru descărcări LIVE STREAM-uri încă nu sunt suportate diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 04b1f83bf..1c6df4fe5 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -81,7 +81,6 @@ Не удалось разобрать веб-сайт Не удалось полностью разобрать веб-сайт Контент недоступен - Заблокировано GEMA Не удалось создать меню загрузки Это прямая трансляция, пока не поддерживается Не удалось загрузить изображение diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index a7359b0d7..a130aa82a 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -52,7 +52,6 @@ Nepodarilo sa dekódovať URL videa Nemožno analyzovať webovú stránku Obsah nie je dostupný - Obsah blokuje GEMA Náhľad videa Náhľad videa diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index ec2a0bea5..d10ae851d 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -62,7 +62,6 @@ Ni mogoče odšifrirati podpisa naslova URL videa Ni mogoče razčleniti spletišča. Vsebina ni na voljo. - Blokirano zaradi pripomb GEMA. Ni mogoče nastaviti menija za prejem datotek. diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index fa3011936..ddef385fa 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -62,7 +62,6 @@ Не могу да дешифрујем потпис видео урл-а Не могу да рашчланим веб-сајт Садржај није доступан - Блокирала ГЕМА Садржај Прикажи старосно-ограничени садржај diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index e99fa65d7..18ceb75a4 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -88,7 +88,6 @@ Det gick inte att analysera webbplatsen Det gick inte att analysera webbplatsen helt Innehållet är inte tillgängligt - Blockerat av GEMA Kunde inte ställa in nedladdningsmenyn Direktsändningar stöds inte än Kunde inte ladda Bild diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index a2dd72807..f1a0fb577 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -59,7 +59,6 @@ Web sitesi ayrıştırılamadı Web sitesi tamamen ayrıştırılamadı İçerik kullanılamıyor - GEMA tarafından engellendi Canlı akışlar henüz desteklenmiyor Herhangi bir akış alınamadı Görüntü yüklenemedi diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index eaca5719a..9a6ab746e 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -61,7 +61,6 @@ Не вдалося проаналізувати веб-сайт Не вдалося повністю проаналізувати веб-сайт Контент недоступний - Заблоковано GEMA Не вдалося налаштувати меню завантаження Трансляції НАЖИВО ще не підтримуються Не вдалося отримати жодного стриму diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index df246a5df..73f7d90bc 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -83,7 +83,6 @@ Không thể phân tích cú pháp trang web Không thể phân tích cú pháp hoàn toàn trang web Nội dung không khả dụng - Chặn bởi GEMA Không thể thiết lập menu tải về Livestream chưa được hỗ trợ Không thể lấy bất kỳ luồng nào diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 55cfad9ef..b44a80e11 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -64,7 +64,6 @@ 无法解析网站 无法完全解析网站 内容不可用 - 已被 GEMA 屏蔽 无法设置下载菜单 这是一个在线流媒体,尚不支持。 无法获取任何流媒体 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 506705536..6780e2b7a 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -55,7 +55,6 @@ 未能建立下載路徑「%1$s」 已建立下載路徑「%1$s」 - 內容被 GEMA 封鎖 按一下搜尋按鈕以開始操作 自動撥放 當其他應用程式要求播放影片時,NewPipe 將會自動播放 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 2d46f4516..6bd36d385 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -99,7 +99,6 @@ 無法解析網站 無法完全解析網站 內容無法使用 - 已被 GEMA 阻擋 無法設定下載選單 尚未支援現場串流 無法取得串流 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 90554a04f..d4d331d9d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -194,7 +194,7 @@ Could not parse website Could not parse website completely Content unavailable - Blocked by GEMA + Could not set up download menu Live streams are not supported yet Could not get any stream diff --git a/assets/gruese_die_gema.svg b/assets/gruese_die_gema.svg deleted file mode 100644 index 067879b34..000000000 --- a/assets/gruese_die_gema.svg +++ /dev/null @@ -1,200 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Genervt? Gib der Alternative c3s - - eine Chance. - - From 41fb6f54642c04ff450a91a051f852efeeaf947a Mon Sep 17 00:00:00 2001 From: Vasiliy Date: Sat, 27 Apr 2019 18:01:18 +0300 Subject: [PATCH 06/30] Update states in lists --- .../stream/model/StreamStateEntity.java | 9 +++++++ .../fragments/list/BaseListFragment.java | 2 ++ .../newpipe/info_list/InfoListAdapter.java | 24 +++++++++++++++++++ .../newpipe/local/BaseLocalListFragment.java | 1 + .../newpipe/local/LocalItemListAdapter.java | 22 +++++++++++++++++ 5 files changed, 58 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java index 946ee1182..d46d5cd74 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java @@ -4,6 +4,7 @@ package org.schabi.newpipe.database.stream.model; import android.arch.persistence.room.ColumnInfo; import android.arch.persistence.room.Entity; import android.arch.persistence.room.ForeignKey; +import android.support.annotation.Nullable; import java.util.concurrent.TimeUnit; @@ -62,4 +63,12 @@ public class StreamStateEntity { 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; + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index 5a49cce28..53d549a46 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -100,6 +100,8 @@ public abstract class BaseListFragment extends BaseStateFragment implem } updateFlags = 0; } + + infoListAdapter.updateStates(); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index df207abb5..8a30c998a 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -31,6 +31,7 @@ import org.schabi.newpipe.util.OnClickGesture; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; @@ -195,6 +196,29 @@ public class InfoListAdapter extends RecyclerView.Adapter { + if (streamStateEntities.size() == states.size()) { + for (int i = 0; i < states.size(); i++) { + final StreamStateEntity newState = streamStateEntities.get(i); + if (!Objects.equals(states.get(i), newState)) { + states.set(i, newState); + notifyItemChanged(header == null ? i : i + 1); + } + } + } else { + //oops, something is wrong + } + }) + ); + } + public void clearStreamItemList() { if (infoItemList.isEmpty()) { return; diff --git a/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java b/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java index 20676c6db..75d49e466 100644 --- a/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java @@ -76,6 +76,7 @@ public abstract class BaseLocalListFragment extends BaseStateFragment } updateFlags = 0; } + itemListAdapter.updateStates(); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java index 372392d7b..80d008231 100644 --- a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java @@ -26,6 +26,7 @@ import org.schabi.newpipe.util.OnClickGesture; import java.text.DateFormat; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; @@ -136,6 +137,27 @@ public class LocalItemListAdapter extends RecyclerView.Adapter { + if (streamStateEntities.size() == states.size()) { + for (int i = 0; i < states.size(); i++) { + final StreamStateEntity newState = streamStateEntities.get(i); + if (!Objects.equals(states.get(i), newState)) { + states.set(i, newState); + notifyItemChanged(header == null ? i : i + 1); + } + } + } else { + //oops, something is wrong + } + }) + ); + } + public void removeItem(final LocalItem data) { final int index = localItems.indexOf(data); From c7cd9e86ac41b08ca742bb583dfb197275e406de Mon Sep 17 00:00:00 2001 From: Vasiliy Date: Sat, 27 Apr 2019 19:04:13 +0300 Subject: [PATCH 07/30] Option to disable states indicators --- .../newpipe/info_list/InfoListAdapter.java | 82 ++++++++++++------- .../newpipe/local/LocalItemListAdapter.java | 32 ++++++-- app/src/main/res/values-ru/strings.xml | 2 + app/src/main/res/values-uk/strings.xml | 2 + app/src/main/res/values/settings_keys.xml | 1 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/history_settings.xml | 8 ++ 7 files changed, 93 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index 8a30c998a..e74a6f2ba 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -1,12 +1,16 @@ package org.schabi.newpipe.info_list; import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.View; import android.view.ViewGroup; +import org.schabi.newpipe.R; import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; @@ -128,13 +132,19 @@ public class InfoListAdapter extends RecyclerView.Adapter data) { - stateLoaders.add( - historyRecordManager.loadStreamStateBatch(data) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(streamStateEntities -> { - addInfoItemList(data, streamStateEntities); - }) - ); + if (isPlaybackStatesVisible()) { + stateLoaders.add( + historyRecordManager.loadStreamStateBatch(data) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(streamStateEntities -> { + addInfoItemList(data, streamStateEntities); + }) + ); + } else { + final ArrayList states = new ArrayList<>(data.size()); + for (int i = data.size(); i > 0; i--) states.add(null); + addInfoItemList(data, states); + } } private void addInfoItemList(List data, List statesEntities) { @@ -163,13 +173,17 @@ public class InfoListAdapter extends RecyclerView.Adapter { - addInfoItem(data, streamStateEntity[0]); - }) - ); + if (isPlaybackStatesVisible()) { + stateLoaders.add( + historyRecordManager.loadStreamState(data) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(streamStateEntity -> { + addInfoItem(data, streamStateEntity[0]); + }) + ); + } else { + addInfoItem(data, null); + } } private void addInfoItem(InfoItem data, StreamStateEntity state) { @@ -200,23 +214,25 @@ public class InfoListAdapter extends RecyclerView.Adapter { - if (streamStateEntities.size() == states.size()) { - for (int i = 0; i < states.size(); i++) { - final StreamStateEntity newState = streamStateEntities.get(i); - if (!Objects.equals(states.get(i), newState)) { - states.set(i, newState); - notifyItemChanged(header == null ? i : i + 1); + if (isPlaybackStatesVisible()) { + stateLoaders.add( + historyRecordManager.loadStreamStateBatch(infoItemList) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe((streamStateEntities) -> { + if (streamStateEntities.size() == states.size()) { + for (int i = 0; i < states.size(); i++) { + final StreamStateEntity newState = streamStateEntities.get(i); + if (!Objects.equals(states.get(i), newState)) { + states.set(i, newState); + notifyItemChanged(header == null ? i : i + 1); + } } + } else { + //oops, something is wrong } - } else { - //oops, something is wrong - } - }) - ); + }) + ); + } } public void clearStreamItemList() { @@ -363,4 +379,12 @@ public class InfoListAdapter extends RecyclerView.Adapter data) { - stateLoaders.add( - historyRecordManager.loadLocalStreamStateBatch(data) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(streamStateEntities -> - addItems(data, streamStateEntities)) - ); + if (isPlaybackStatesVisible()) { + stateLoaders.add( + historyRecordManager.loadLocalStreamStateBatch(data) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(streamStateEntities -> + addItems(data, streamStateEntities)) + ); + } else { + final ArrayList states = new ArrayList<>(data.size()); + for (int i = data.size(); i > 0; i--) states.add(null); + addItems(data, states); + } } private void addItems(List data, List streamStates) { @@ -138,7 +148,7 @@ public class LocalItemListAdapter extends RecyclerView.AdapterИстория просмотров Продолжать воспроизведение Восстанавливать с последней позиции + Позиции в списках + Отображать индикаторы позиций просмотра в списках Очистить данные Запоминать воспроизведённые потоки Возобновить при фокусе diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 41e6f22ba..0ab0ba78d 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -156,6 +156,8 @@ Історія переглядiв Продовживати перегляд Відновлювати останню позицію + Позиції у списках + Відображати індикатори позицій переглядів у списках Очистити дані Вести облік перегляду відеозаписів Відновити відтворення diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index b70305d56..dd9c30080 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -151,6 +151,7 @@ enable_watch_history main_page_content enable_playback_resume + enable_playback_state_lists import_data export_data diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4f2ef075c..d92dbeb09 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -98,6 +98,8 @@ Watch history Resume playback Restore last playback position + Positions in lists + Show playback position indicators in lists Clear data Keep track of watched videos Resume on focus gain diff --git a/app/src/main/res/xml/history_settings.xml b/app/src/main/res/xml/history_settings.xml index 305b1c360..cae2d56c0 100644 --- a/app/src/main/res/xml/history_settings.xml +++ b/app/src/main/res/xml/history_settings.xml @@ -19,6 +19,14 @@ android:title="@string/enable_playback_resume_title" app:iconSpaceReserved="false" /> + + Date: Sat, 27 Apr 2019 21:23:52 +0300 Subject: [PATCH 08/30] Refactor adapter --- .../newpipe/info_list/InfoListAdapter.java | 173 ++++++---------- .../info_list/StateObjectsListAdapter.java | 189 ++++++++++++++++++ .../newpipe/local/LocalItemListAdapter.java | 122 ++++------- .../schabi/newpipe/util/SparseArrayUtils.java | 30 +++ 4 files changed, 314 insertions(+), 200 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/info_list/StateObjectsListAdapter.java create mode 100644 app/src/main/java/org/schabi/newpipe/util/SparseArrayUtils.java diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index e74a6f2ba..3cd06f3d6 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -1,16 +1,14 @@ package org.schabi.newpipe.info_list; import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.View; import android.view.ViewGroup; -import org.schabi.newpipe.R; import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; @@ -29,16 +27,11 @@ import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamGridInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder; -import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.util.FallbackViewHolder; import org.schabi.newpipe.util.OnClickGesture; import java.util.ArrayList; import java.util.List; -import java.util.Objects; - -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; /* * Created by Christian Schabesberger on 01.08.16. @@ -60,7 +53,7 @@ import io.reactivex.disposables.CompositeDisposable; * along with NewPipe. If not, see . */ -public class InfoListAdapter extends RecyclerView.Adapter { +public class InfoListAdapter extends StateObjectsListAdapter { private static final String TAG = InfoListAdapter.class.getSimpleName(); private static final boolean DEBUG = false; @@ -80,10 +73,7 @@ public class InfoListAdapter extends RecyclerView.Adapter infoItemList; - private final ArrayList states; - private final CompositeDisposable stateLoaders; private boolean useMiniVariant = false; private boolean useGridVariant = false; private boolean showFooter = false; @@ -100,11 +90,9 @@ public class InfoListAdapter extends RecyclerView.Adapter(); - states = new ArrayList<>(); - stateLoaders = new CompositeDisposable(); } public void setOnStreamSelectedListener(OnClickGesture listener) { @@ -131,107 +119,64 @@ public class InfoListAdapter extends RecyclerView.Adapter data) { - if (isPlaybackStatesVisible()) { - stateLoaders.add( - historyRecordManager.loadStreamStateBatch(data) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(streamStateEntities -> { - addInfoItemList(data, streamStateEntities); - }) - ); - } else { - final ArrayList states = new ArrayList<>(data.size()); - for (int i = data.size(); i > 0; i--) states.add(null); - addInfoItemList(data, states); - } - } - - private void addInfoItemList(List data, List statesEntities) { + public void addInfoItemList(@Nullable final List data) { if (data != null) { - if (DEBUG) { - Log.d(TAG, "addInfoItemList() before > infoItemList.size() = " + infoItemList.size() + ", data.size() = " + data.size()); - } - - int offsetStart = sizeConsideringHeaderOffset(); - infoItemList.addAll(data); - states.addAll(statesEntities); - - 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); - } + loadStates(data, infoItemList.size(), () -> addInfoItemListImpl(data)); } } - public void addInfoItem(InfoItem data) { - if (isPlaybackStatesVisible()) { - stateLoaders.add( - historyRecordManager.loadStreamState(data) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(streamStateEntity -> { - addInfoItem(data, streamStateEntity[0]); - }) - ); - } else { - addInfoItem(data, null); + private void addInfoItemListImpl(@NonNull List 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); } } - private void addInfoItem(InfoItem data, StreamStateEntity state) { + public void addInfoItem(@Nullable InfoItem data) { if (data != null) { - if (DEBUG) { - Log.d(TAG, "addInfoItem() before > infoItemList.size() = " + infoItemList.size() + ", thread = " + Thread.currentThread()); - } + loadState(data, infoItemList.size(), () -> addInfoItemImpl(data)); + } + } - int positionInserted = sizeConsideringHeaderOffset(); - infoItemList.add(data); - states.add(state); + private void addInfoItemImpl(@NonNull InfoItem data) { + if (DEBUG) { + Log.d(TAG, "addInfoItem() before > infoItemList.size() = " + infoItemList.size() + ", thread = " + Thread.currentThread()); + } - if (DEBUG) { - Log.d(TAG, "addInfoItem() after > position = " + positionInserted + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter); - } - notifyItemInserted(positionInserted); + int positionInserted = sizeConsideringHeaderOffset(); + infoItemList.add(data); - if (footer != null && showFooter) { - int footerNow = sizeConsideringHeaderOffset(); - notifyItemMoved(positionInserted, footerNow); + if (DEBUG) { + Log.d(TAG, "addInfoItem() after > position = " + positionInserted + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter); + } + 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()) { - return; - } - if (isPlaybackStatesVisible()) { - stateLoaders.add( - historyRecordManager.loadStreamStateBatch(infoItemList) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe((streamStateEntities) -> { - if (streamStateEntities.size() == states.size()) { - for (int i = 0; i < states.size(); i++) { - final StreamStateEntity newState = streamStateEntities.get(i); - if (!Objects.equals(states.get(i), newState)) { - states.set(i, newState); - notifyItemChanged(header == null ? i : i + 1); - } - } - } else { - //oops, something is wrong - } - }) - ); + if (!infoItemList.isEmpty()) { + updateAllStates(infoItemList); } } @@ -240,7 +185,7 @@ public class InfoListAdapter extends RecyclerView.Adapter { + + private final SparseArray 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 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 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 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 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 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 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); + +} diff --git a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java index 6dddcb714..903712af2 100644 --- a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java @@ -1,19 +1,17 @@ package org.schabi.newpipe.local; import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.View; import android.view.ViewGroup; -import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.stream.model.StreamStateEntity; -import org.schabi.newpipe.local.history.HistoryRecordManager; +import org.schabi.newpipe.info_list.StateObjectsListAdapter; import org.schabi.newpipe.local.holder.LocalItemHolder; import org.schabi.newpipe.local.holder.LocalPlaylistGridItemHolder; import org.schabi.newpipe.local.holder.LocalPlaylistItemHolder; @@ -30,10 +28,6 @@ import org.schabi.newpipe.util.OnClickGesture; import java.text.DateFormat; import java.util.ArrayList; import java.util.List; -import java.util.Objects; - -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; /* * Created by Christian Schabesberger on 01.08.16. @@ -55,7 +49,7 @@ import io.reactivex.disposables.CompositeDisposable; * along with NewPipe. If not, see . */ -public class LocalItemListAdapter extends RecyclerView.Adapter { +public class LocalItemListAdapter extends StateObjectsListAdapter { private static final String TAG = LocalItemListAdapter.class.getSimpleName(); private static final boolean DEBUG = false; @@ -73,10 +67,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter localItems; - private final ArrayList states; - private final CompositeDisposable stateLoaders; private final DateFormat dateFormat; private boolean showFooter = false; @@ -85,13 +76,11 @@ public class LocalItemListAdapter extends RecyclerView.Adapter(); dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Localization.getPreferredLocale(activity)); - states = new ArrayList<>(); - stateLoaders = new CompositeDisposable(); } public void setSelectedListener(OnClickGesture listener) { @@ -102,76 +91,49 @@ public class LocalItemListAdapter extends RecyclerView.Adapter data) { - if (isPlaybackStatesVisible()) { - stateLoaders.add( - historyRecordManager.loadLocalStreamStateBatch(data) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(streamStateEntities -> - addItems(data, streamStateEntities)) - ); - } else { - final ArrayList states = new ArrayList<>(data.size()); - for (int i = data.size(); i > 0; i--) states.add(null); - addItems(data, states); + public void addItems(@Nullable List data) { + if (data != null) { + loadStatesForLocal(data, localItems.size(), () -> addItemsImpl(data)); } } - private void addItems(List data, List streamStates) { - if (data != null) { - if (DEBUG) { - Log.d(TAG, "addItems() before > localItems.size() = " + - localItems.size() + ", data.size() = " + data.size()); - } + private void addItemsImpl(@NonNull List data) { + if (DEBUG) { + Log.d(TAG, "addItems() before > localItems.size() = " + + localItems.size() + ", data.size() = " + data.size()); + } - int offsetStart = sizeConsideringHeader(); - localItems.addAll(data); - states.addAll(streamStates); + int offsetStart = sizeConsideringHeader(); + localItems.addAll(data); - if (DEBUG) { - Log.d(TAG, "addItems() after > offsetStart = " + offsetStart + - ", localItems.size() = " + localItems.size() + - ", header = " + header + ", footer = " + footer + - ", showFooter = " + showFooter); - } + if (DEBUG) { + Log.d(TAG, "addItems() after > offsetStart = " + offsetStart + + ", localItems.size() = " + localItems.size() + + ", header = " + header + ", footer = " + footer + + ", showFooter = " + showFooter); + } - notifyItemRangeInserted(offsetStart, data.size()); + notifyItemRangeInserted(offsetStart, data.size()); - if (footer != null && showFooter) { - int footerNow = sizeConsideringHeader(); - notifyItemMoved(offsetStart, footerNow); + if (footer != null && showFooter) { + int footerNow = sizeConsideringHeader(); + notifyItemMoved(offsetStart, footerNow); - if (DEBUG) Log.d(TAG, "addItems() footer from " + offsetStart + - " to " + footerNow); - } + if (DEBUG) Log.d(TAG, "addItems() footer from " + offsetStart + + " to " + footerNow); } } public void updateStates() { - if (localItems.isEmpty() || !isPlaybackStatesVisible()) return; - stateLoaders.add( - historyRecordManager.loadLocalStreamStateBatch(localItems) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe((streamStateEntities) -> { - if (streamStateEntities.size() == states.size()) { - for (int i = 0; i < states.size(); i++) { - final StreamStateEntity newState = streamStateEntities.get(i); - if (!Objects.equals(states.get(i), newState)) { - states.set(i, newState); - notifyItemChanged(header == null ? i : i + 1); - } - } - } else { - //oops, something is wrong - } - }) - ); + if (!localItems.isEmpty()) { + updateAllLocalStates(localItems); + } } public void removeItem(final LocalItem data) { final int index = localItems.indexOf(data); - localItems.remove(index); + removeState(index); notifyItemRemoved(index + (header != null ? 1 : 0)); } @@ -183,7 +145,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter= localItems.size() || actualTo >= localItems.size()) return false; localItems.add(actualTo, localItems.remove(actualFrom)); - states.add(actualTo, states.remove(actualFrom)); + moveState(actualFrom, actualTo); notifyItemMoved(fromAdapterPosition, toAdapterPosition); return true; } @@ -193,6 +155,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter void shiftItemsDown(SparseArray 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 void shiftItemsUp(SparseArray 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 int[] getKeys(SparseArray sparseArray) { + final int[] result = new int[sparseArray.size()]; + for (int i = 0; i < result.length; i++) { + result[i] = sparseArray.keyAt(i); + } + return result; + } +} From 93f25181596792ed35287cd12fff55d97aa6ffe0 Mon Sep 17 00:00:00 2001 From: Vasiliy Date: Sat, 27 Apr 2019 22:27:08 +0300 Subject: [PATCH 09/30] Animate states changed --- .../fragments/detail/VideoDetailFragment.java | 2 +- .../fragments/list/BaseListFragment.java | 2 +- .../newpipe/info_list/InfoListAdapter.java | 19 +++++++++++++++- .../info_list/holder/InfoItemHolder.java | 3 +++ .../holder/StreamMiniInfoItemHolder.java | 21 ++++++++++++++++-- .../newpipe/local/BaseLocalListFragment.java | 3 ++- .../newpipe/local/LocalItemListAdapter.java | 19 +++++++++++++++- .../newpipe/local/holder/LocalItemHolder.java | 3 +++ .../holder/LocalPlaylistStreamItemHolder.java | 22 +++++++++++++++++-- .../LocalStatisticStreamItemHolder.java | 22 +++++++++++++++++-- .../subscription/SubscriptionFragment.java | 2 ++ .../newpipe/views/AnimatedProgressBar.java | 9 ++------ .../main/res/layout/list_stream_grid_item.xml | 2 +- app/src/main/res/layout/list_stream_item.xml | 2 +- .../main/res/layout/list_stream_mini_item.xml | 2 +- .../res/layout/list_stream_playlist_item.xml | 2 +- 16 files changed, 113 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index d6630c9c3..42ac62c59 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -1287,7 +1287,7 @@ public class VideoDetailFragment .subscribe(state -> { final int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()); positionView.setMax((int) info.getDuration()); - positionView.setProgress(seconds); + positionView.setProgressAnimated(seconds); detailPositionView.setText(Localization.getDurationString(seconds)); animateView(positionView, true, 500); animateView(detailPositionView, true, 500); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index 53d549a46..d9c58fbf4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -101,7 +101,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem updateFlags = 0; } - infoListAdapter.updateStates(); + itemsList.post(infoListAdapter::updateStates); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index 3cd06f3d6..7f5b07dbe 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -312,9 +312,26 @@ public class InfoListAdapter extends StateObjectsListAdapter { } } + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List 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); + notifyItemChanged(header == null ? position : position + 1, state != null ? state : false); } public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final int spanCount) { diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java index 969d2682e..3bc0d9e54 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java @@ -38,4 +38,7 @@ public abstract class InfoItemHolder extends RecyclerView.ViewHolder { } public abstract void updateFromItem(final InfoItem infoItem, @Nullable final StreamStateEntity state); + + public void updateState(final InfoItem infoItem, @Nullable final StreamStateEntity state) { + } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java index a59cb009f..aa2a3f878 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java @@ -5,7 +5,6 @@ import android.support.v4.content.ContextCompat; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; -import android.widget.ProgressBar; import android.widget.TextView; import org.schabi.newpipe.R; @@ -14,8 +13,10 @@ import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; +import org.schabi.newpipe.views.AnimatedProgressBar; import java.util.concurrent.TimeUnit; @@ -25,7 +26,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { public final TextView itemVideoTitleView; public final TextView itemUploaderView; public final TextView itemDurationView; - public final ProgressBar itemProgressView; + public final AnimatedProgressBar itemProgressView; StreamMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { super(infoItemBuilder, layoutId, parent); @@ -99,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) { itemView.setLongClickable(true); itemView.setOnLongClickListener(view -> { diff --git a/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java b/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java index 75d49e466..94672bd49 100644 --- a/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java @@ -76,7 +76,8 @@ public abstract class BaseLocalListFragment extends BaseStateFragment } updateFlags = 0; } - itemListAdapter.updateStates(); + + itemsList.post(itemListAdapter::updateStates); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java index 903712af2..d29e85ee3 100644 --- a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java @@ -285,9 +285,26 @@ public class LocalItemListAdapter extends StateObjectsListAdapter { } } + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List 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); + notifyItemChanged(header == null ? position : position + 1, state != null ? state : false); } public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final int spanCount) { diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalItemHolder.java index 01af60f98..c00fa1fb4 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalItemHolder.java @@ -41,4 +41,7 @@ public abstract class LocalItemHolder extends RecyclerView.ViewHolder { } public abstract void updateFromItem(final LocalItem item, @Nullable final StreamStateEntity state, final DateFormat dateFormat); + + public void updateState(final LocalItem localItem, @Nullable final StreamStateEntity state) { + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java index 48bbbc81d..0c4e66c9d 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java @@ -6,7 +6,6 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; -import android.widget.ProgressBar; import android.widget.TextView; import org.schabi.newpipe.R; @@ -15,8 +14,10 @@ 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.local.LocalItemBuilder; +import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; +import org.schabi.newpipe.views.AnimatedProgressBar; import java.text.DateFormat; import java.util.concurrent.TimeUnit; @@ -28,7 +29,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { public final TextView itemAdditionalDetailsView; public final TextView itemDurationView; public final View itemHandleView; - public final ProgressBar itemProgressView; + public final AnimatedProgressBar itemProgressView; LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { super(infoItemBuilder, layoutId, parent); @@ -92,6 +93,23 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { 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) { return (view, motionEvent) -> { view.performClick(); diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java index ac194b776..b24051a4f 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java @@ -5,7 +5,6 @@ import android.support.v4.content.ContextCompat; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; -import android.widget.ProgressBar; import android.widget.TextView; import org.schabi.newpipe.R; @@ -14,8 +13,10 @@ 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.local.LocalItemBuilder; +import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; +import org.schabi.newpipe.views.AnimatedProgressBar; import java.text.DateFormat; import java.util.concurrent.TimeUnit; @@ -48,7 +49,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { public final TextView itemDurationView; @Nullable public final TextView itemAdditionalDetails; - public final ProgressBar itemProgressView; + public final AnimatedProgressBar itemProgressView; public LocalStatisticStreamItemHolder(LocalItemBuilder itemBuilder, ViewGroup parent) { this(itemBuilder, R.layout.list_stream_item, parent); @@ -121,4 +122,21 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { 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); + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java index 364d50df6..b00ea05ea 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java @@ -153,6 +153,8 @@ public class SubscriptionFragment extends BaseStateFragment - - - - Date: Mon, 6 May 2019 19:16:39 +0300 Subject: [PATCH 10/30] Fix tablet ui --- .../res/layout-large-land/fragment_video_detail.xml | 11 +++++------ .../res/layout/list_stream_playlist_grid_item.xml | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/src/main/res/layout-large-land/fragment_video_detail.xml b/app/src/main/res/layout-large-land/fragment_video_detail.xml index f773c69cf..15d6b7a17 100644 --- a/app/src/main/res/layout-large-land/fragment_video_detail.xml +++ b/app/src/main/res/layout-large-land/fragment_video_detail.xml @@ -6,7 +6,9 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:focusableInTouchMode="true" - android:orientation="horizontal"> + tools:ignore="RtlHardcoded" + android:orientation="horizontal" + android:baselineAligned="false"> @@ -177,7 +178,6 @@ android:paddingBottom="8dp" android:textAppearance="?android:attr/textAppearanceLarge" android:textSize="@dimen/video_item_detail_title_text_size" - tools:ignore="RtlHardcoded" tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed a ultricies ex. Integer sit amet sodales risus. Duis non mi et urna pretium bibendum. Nunc eleifend est quis ipsum porttitor egestas. Sed facilisis, nisl quis eleifend pellentesque, orci metus egestas dolor, at accumsan eros metus quis libero." /> + tools:ignore="ContentDescription" /> @@ -253,8 +253,7 @@ android:layout_width="@dimen/video_item_detail_uploader_image_size" android:layout_height="@dimen/video_item_detail_uploader_image_size" android:contentDescription="@string/detail_uploader_thumbnail_view_description" - android:src="@drawable/buddy" - tools:ignore="RtlHardcoded" /> + android:src="@drawable/buddy" /> - Date: Tue, 7 May 2019 13:57:31 +0530 Subject: [PATCH 11/30] Update strings.xml --- app/src/main/res/values/strings.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d4d331d9d..5a303ef25 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -194,7 +194,6 @@ Could not parse website Could not parse website completely Content unavailable - Could not set up download menu Live streams are not supported yet Could not get any stream @@ -603,4 +602,4 @@ Downloads that can not be paused will be restarted Close - \ No newline at end of file + From 9e34fee58c1bd2ba9040a5457f73b9e7cc36a256 Mon Sep 17 00:00:00 2001 From: kapodamy Date: Fri, 22 Mar 2019 22:54:07 -0300 Subject: [PATCH 12/30] New MP4 muxer + Queue changes + Storage fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Main changes: * correctly check the available space (CircularFile.java) * misc cleanup (CircularFile.java) * use the "Error Reporter" for non-http errors * rewrite network state checking and add better support for API 21 (Lollipop) or higher * implement "metered networks" * add buttons in "Downloads" activity to start/pause all pending downloads, ignoring the queue flag or if the network is "metered" * add workaround for VPN connections and/or network switching. Example: switching WiFi to 3G * rewrite DataReader ¡Webm muxer is now 57% more faster! * rewrite CircularFile, use file buffers instead of memory buffers. Less troubles in low-end devices * fix missing offset for KaxCluster (WebMWriter.java), manifested as no thumbnails on file explorers Download queue: * remember queue status, unless the user pause the download (un-queue) * semi-automatic downloads, between networks. Effective if the user create a new download or the downloads activity is starts * allow enqueue failed downloads * new option, queue limit, enabled by default. Used to allow one or multiple downloads at same time Miscellaneous: * fix crash while selecting details/error menu (mistake on MissionFragment.java) * misc serialize changes (DownloadMission.java) * minor UI tweaks * allow overwrite paused downloads * fix wrong icons for grid/list button in downloads * add share option * implement #2006 * correct misspelled word in strings.xml (es) (cmn) * fix MissionAdapter crash during device shutdown New Mp4Muxer + required changes: * new mp4 muxer (from dash only) with this, muxing on Android 7 is possible now!!! * re-work in SharpStream * drop mp4 dash muxer * misc changes: add warning in SecondaryStreamHelper.java, * strip m4a DASH files to normal m4a format (youtube only) Fix storage issues: * warn to the user if is choosing a "read only" download directory (for external SD Cards), useless is rooted :) * "write proof" allow post-processing resuming only if the device ran out of space * implement "insufficient storage" error for downloads --- .../newpipe/download/DownloadDialog.java | 61 +- .../org/schabi/newpipe/report/UserAction.java | 4 +- .../settings/DownloadSettingsFragment.java | 14 +- .../schabi/newpipe/streams/DataReader.java | 237 ++++- .../schabi/newpipe/streams/Mp4DashReader.java | 444 +++++++--- .../schabi/newpipe/streams/Mp4DashWriter.java | 623 -------------- .../newpipe/streams/Mp4FromDashWriter.java | 810 ++++++++++++++++++ .../newpipe/streams/SubtitleConverter.java | 15 +- .../newpipe/streams/TrackDataChunk.java | 65 -- .../schabi/newpipe/streams/WebMReader.java | 16 +- .../schabi/newpipe/streams/WebMWriter.java | 130 ++- .../newpipe/streams/io/SharpStream.java | 22 +- .../org/schabi/newpipe/util/ListHelper.java | 14 +- .../newpipe/util/SecondaryStreamHelper.java | 2 +- .../giga/get/DownloadInitializer.java | 3 - .../us/shandian/giga/get/DownloadMission.java | 122 ++- .../java/us/shandian/giga/get/Mission.java | 5 + .../giga/postprocessing/M4aNoDash.java | 43 + ...p4DashMuxer.java => Mp4FromDashMuxer.java} | 58 +- .../giga/postprocessing/Mp4Muxer.java | 136 --- .../giga/postprocessing/Postprocessing.java | 118 ++- .../io/ChunkFileInputStream.java | 5 +- .../giga/postprocessing/io/CircularFile.java | 375 -------- .../postprocessing/io/CircularFileWriter.java | 459 ++++++++++ .../giga/postprocessing/io/FileStream.java | 126 --- .../postprocessing/io/SharpInputStream.java | 4 +- .../giga/service/DownloadManager.java | 227 +++-- .../giga/service/DownloadManagerService.java | 143 ++-- .../giga/ui/adapter/MissionAdapter.java | 255 ++++-- .../giga/ui/fragment/MissionsFragment.java | 33 +- .../res/drawable-hdpi/ic_pause_black_24dp.png | Bin 0 -> 135 bytes .../res/drawable-hdpi/ic_pause_white_24dp.png | Bin 0 -> 138 bytes .../res/drawable-mdpi/ic_pause_black_24dp.png | Bin 0 -> 109 bytes .../res/drawable-mdpi/ic_pause_white_24dp.png | Bin 0 -> 112 bytes .../drawable-xhdpi/ic_pause_black_24dp.png | Bin 0 -> 162 bytes .../drawable-xhdpi/ic_pause_white_24dp.png | Bin 0 -> 139 bytes .../drawable-xxhdpi/ic_pause_black_24dp.png | Bin 0 -> 196 bytes .../drawable-xxhdpi/ic_pause_white_24dp.png | Bin 0 -> 206 bytes .../drawable-xxxhdpi/ic_pause_black_24dp.png | Bin 0 -> 248 bytes .../drawable-xxxhdpi/ic_pause_white_24dp.png | Bin 0 -> 254 bytes app/src/main/res/menu/download_menu.xml | 12 + app/src/main/res/menu/mission.xml | 13 +- app/src/main/res/values-cmn/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 30 +- app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/settings_keys.xml | 1 + app/src/main/res/values/strings.xml | 14 +- app/src/main/res/values/styles.xml | 2 + app/src/main/res/xml/download_settings.xml | 7 + 49 files changed, 2715 insertions(+), 1936 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/streams/Mp4DashWriter.java create mode 100644 app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java delete mode 100644 app/src/main/java/org/schabi/newpipe/streams/TrackDataChunk.java create mode 100644 app/src/main/java/us/shandian/giga/postprocessing/M4aNoDash.java rename app/src/main/java/us/shandian/giga/postprocessing/{Mp4DashMuxer.java => Mp4FromDashMuxer.java} (60%) delete mode 100644 app/src/main/java/us/shandian/giga/postprocessing/Mp4Muxer.java delete mode 100644 app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java create mode 100644 app/src/main/java/us/shandian/giga/postprocessing/io/CircularFileWriter.java delete mode 100644 app/src/main/java/us/shandian/giga/postprocessing/io/FileStream.java create mode 100644 app/src/main/res/drawable-hdpi/ic_pause_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_pause_white_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_pause_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_pause_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_pause_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_pause_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_pause_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_pause_white_24dp.png diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index ec6d42b29..0b4767133 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -7,6 +7,7 @@ import android.preference.PreferenceManager; import android.support.annotation.IdRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.StringRes; import android.support.v4.app.DialogFragment; import android.support.v7.app.AlertDialog; import android.support.v7.widget.Toolbar; @@ -52,6 +53,7 @@ import icepick.State; import io.reactivex.disposables.CompositeDisposable; import us.shandian.giga.postprocessing.Postprocessing; import us.shandian.giga.service.DownloadManagerService; +import us.shandian.giga.service.DownloadManagerService.MissionCheck; public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener { private static final String TAG = "DialogFragment"; @@ -263,7 +265,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck } @Override - public void onSaveInstanceState(Bundle outState) { + public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); Icepick.saveInstanceState(this, outState); } @@ -476,23 +478,40 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck final String finalFileName = fileName; - DownloadManagerService.checkForRunningMission(context, location, fileName, (listed, finished) -> { - if (listed) { - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.download_dialog_title) - .setMessage(finished ? R.string.overwrite_warning : R.string.download_already_running) - .setPositiveButton( - finished ? R.string.overwrite : R.string.generate_unique_name, - (dialog, which) -> downloadSelected(context, stream, location, finalFileName, kind, threads) - ) - .setNegativeButton(android.R.string.cancel, (dialog, which) -> { - dialog.cancel(); - }) - .create() - .show(); - } else { - downloadSelected(context, stream, location, finalFileName, kind, threads); + DownloadManagerService.checkForRunningMission(context, location, fileName, (MissionCheck result) -> { + @StringRes int msgBtn; + @StringRes int msgBody; + + switch (result) { + case Finished: + msgBtn = R.string.overwrite; + msgBody = R.string.overwrite_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; + default: + downloadSelected(context, stream, location, finalFileName, kind, threads); + return; } + + // overwrite or unique name actions are done by the download manager + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.download_dialog_title) + .setMessage(msgBody) + .setPositiveButton( + msgBtn, + (dialog, which) -> downloadSelected(context, stream, location, finalFileName, kind, threads) + ) + .setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel()) + .create() + .show(); }); } @@ -503,14 +522,18 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck String secondaryStreamUrl = null; long nearLength = 0; - if (selectedStream instanceof VideoStream) { + if (selectedStream instanceof AudioStream) { + if (selectedStream.getFormat() == MediaFormat.M4A) { + psName = Postprocessing.ALGORITHM_M4A_NO_DASH; + } + } else if (selectedStream instanceof VideoStream) { SecondaryStreamHelper secondaryStream = videoStreamsAdapter .getAllSecondary() .get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream)); if (secondaryStream != null) { secondaryStreamUrl = secondaryStream.getStream().getUrl(); - psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER; + psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER; psArgs = null; long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream); diff --git a/app/src/main/java/org/schabi/newpipe/report/UserAction.java b/app/src/main/java/org/schabi/newpipe/report/UserAction.java index 2b2369ad3..2cca9305a 100644 --- a/app/src/main/java/org/schabi/newpipe/report/UserAction.java +++ b/app/src/main/java/org/schabi/newpipe/report/UserAction.java @@ -17,7 +17,9 @@ public enum UserAction { REQUESTED_KIOSK("requested kiosk"), REQUESTED_COMMENTS("requested comments"), 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; diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java index 8214d7b4b..82c6853d5 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.settings; import android.app.Activity; +import android.app.AlertDialog; import android.content.Intent; import android.os.Bundle; import android.support.annotation.Nullable; @@ -12,6 +13,8 @@ import com.nononsenseapps.filepicker.Utils; import org.schabi.newpipe.R; import org.schabi.newpipe.util.FilePickerActivityHelper; +import java.io.File; + public class DownloadSettingsFragment extends BasePreferenceFragment { private static final int REQUEST_DOWNLOAD_PATH = 0x1235; private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236; @@ -45,7 +48,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { @Override public boolean onPreferenceTreeClick(Preference preference) { 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) @@ -78,6 +81,15 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { defaultPreferences.edit().putString(key, path).apply(); updatePreferencesSummary(); + + File target = new File(path); + if (!target.canWrite()) { + AlertDialog.Builder msg = new AlertDialog.Builder(getContext()); + msg.setTitle(R.string.download_to_sdcard_error_title); + msg.setMessage(R.string.download_to_sdcard_error_message); + msg.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { }); + msg.show(); + } } } } diff --git a/app/src/main/java/org/schabi/newpipe/streams/DataReader.java b/app/src/main/java/org/schabi/newpipe/streams/DataReader.java index d0e946eb7..567fa5229 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/DataReader.java +++ b/app/src/main/java/org/schabi/newpipe/streams/DataReader.java @@ -1,9 +1,10 @@ package org.schabi.newpipe.streams; +import org.schabi.newpipe.streams.io.SharpStream; + import java.io.EOFException; import java.io.IOException; - -import org.schabi.newpipe.streams.io.SharpStream; +import java.io.InputStream; /** * @author kapodamy @@ -15,89 +16,237 @@ public class DataReader { public final static int INTEGER_SIZE = 4; public final static int FLOAT_SIZE = 4; - private long pos; - 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) { - this.rewind = stream.canRewind(); this.stream = stream; - this.pos = 0L; + this.readOffset = this.readBuffer.length; } 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); return primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3]; } - public final int read() throws IOException { - int value = stream.read(); - if (value == -1) { - throw new EOFException(); - } - - pos++; - return value; + public short readShort() throws IOException { + primitiveRead(SHORT_SIZE); + return (short) (primitive[0] << 8 | primitive[1]); } - public final long skipBytes(long amount) throws IOException { - amount = stream.skip(amount); - pos += amount; - return amount; - } - - public final long readLong() throws IOException { + public long readLong() throws IOException { primitiveRead(LONG_SIZE); 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]; return high << 32 | low; } - public final short readShort() throws IOException { - primitiveRead(SHORT_SIZE); - return (short) (primitive[0] << 8 | primitive[1]); - } - - public final int read(byte[] buffer) throws IOException { + public int read(byte[] buffer) throws IOException { return read(buffer, 0, buffer.length); } - public final int read(byte[] buffer, int offset, int count) throws IOException { - int res = stream.read(buffer, offset, count); - pos += res; + public int read(byte[] buffer, int offset, int count) throws IOException { + if (readCount < 0) { + 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() { - return stream.available() > 0; + public boolean available() { + return readCount > 0 || stream.available() > 0; } public void rewind() throws IOException { stream.rewind(); - pos = 0; + + if ((position - viewSize) > 0) { + viewSize = 0;// drop view + } else { + viewSize += position; + } + + position = 0; + readOffset = readBuffer.length; } 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 { byte[] buffer = new byte[amount]; - int read = stream.read(buffer, 0, amount); - pos += read; + int read = read(buffer, 0, 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++) { - primitive[i] = (short) (buffer[i] & 0xFF);// the "byte" datatype is signed and is very annoying + for (int i = 0; i < amount; i++) { + primitive[i] = (short) (buffer[i] & 0xFF);// the "byte" data type in java is signed and is very annoying } } + + private final byte[] readBuffer = new byte[8 * 1024]; + 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; + } + } diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java index 271929d47..c52ebf3aa 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java +++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java @@ -1,17 +1,15 @@ package org.schabi.newpipe.streams; +import org.schabi.newpipe.streams.io.SharpStream; + import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; - import java.nio.ByteBuffer; - import java.util.ArrayList; import java.util.NoSuchElementException; -import org.schabi.newpipe.streams.io.SharpStream; - /** * @author kapodamy */ @@ -35,14 +33,29 @@ public class Mp4DashReader { private static final int ATOM_TREX = 0x74726578; private static final int ATOM_TKHD = 0x746B6864; 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_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_ISO5 = 0x69736F35; + + private static final int HANDLER_VIDE = 0x76696465; + private static final int HANDLER_SOUN = 0x736F756E; + private static final int HANDLER_SUBT = 0x73756274; // private final DataReader stream; private Mp4Track[] tracks = null; + private int[] brands = null; private Box box; private Moof moof; @@ -50,9 +63,10 @@ public class Mp4DashReader { private boolean chunkZero = false; private int selectedTrack = -1; + private Box backupBox = null; public enum TrackKind { - Audio, Video, Other + Audio, Video, Subtitles, Other } public Mp4DashReader(SharpStream source) { @@ -65,8 +79,15 @@ public class Mp4DashReader { } box = readBox(ATOM_FTYP); - if (parse_ftyp() != BRAND_DASH) { - throw new NoSuchElementException("Main Brand is not dash"); + brands = parse_ftyp(box); + 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; @@ -84,8 +105,6 @@ public class Mp4DashReader { break; case ATOM_MFRA: 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) { - tracks[i].kind = moov.trak[i].tkhd.bVolume == 0 ? TrackKind.Other : TrackKind.Audio; - } else { - tracks[i].kind = TrackKind.Video; + switch (moov.trak[i].mdia.hdlr.subType) { + case HANDLER_VIDE: + 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; return tracks[index]; } @@ -126,7 +156,7 @@ public class Mp4DashReader { * @return list with a basic info * @throws IOException if the source stream is not seekeable */ - public int getFragmentsCount() throws IOException { + int getFragmentsCount() throws IOException { if (selectedTrack < 0) { throw new IllegalStateException("track no selected"); } @@ -136,7 +166,6 @@ public class Mp4DashReader { Box tmp; int count = 0; - long orig_offset = stream.position(); if (box.type == ATOM_MOOF) { tmp = box; @@ -162,17 +191,36 @@ public class Mp4DashReader { ensure(tmp); } while (stream.available() && (tmp = readBox()) != null); - stream.rewind(); - stream.skipBytes((int) orig_offset); + rewind(); 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() { return tracks; } - public Mp4TrackChunk getNextChunk() throws IOException { + public Mp4DashChunk getNextChunk(boolean infoOnly) throws IOException { Mp4Track track = tracks[selectedTrack]; while (stream.available()) { @@ -208,7 +256,7 @@ public class Mp4DashReader { if (hasFlag(moof.traf.tfhd.bFlags, 0x10)) { moof.traf.trun.chunkSize = moof.traf.tfhd.defaultSampleSize * moof.traf.trun.entryCount; } 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) { @@ -228,9 +276,12 @@ public class Mp4DashReader { continue;// find another chunk } - Mp4TrackChunk chunk = new Mp4TrackChunk(); + Mp4DashChunk chunk = new Mp4DashChunk(); chunk.moof = moof; - chunk.data = new TrackDataChunk(stream, moof.traf.trun.chunkSize); + if (!infoOnly) { + chunk.data = stream.getView(moof.traf.trun.chunkSize); + } + moof = null; stream.skipBytes(chunk.moof.traf.trun.dataOffset); @@ -269,6 +320,10 @@ public class Mp4DashReader { b.size = stream.readInt(); b.type = stream.readInt(); + if (b.size == 1) { + b.size = stream.readLong(); + } + return b; } @@ -280,6 +335,25 @@ public class Mp4DashReader { 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 { long skip = ref.offset + ref.size - stream.position(); @@ -310,6 +384,14 @@ public class Mp4DashReader { return null; } + private Box untilAnyBox(Box ref) throws IOException { + if (stream.position() >= (ref.offset + ref.size)) { + return null; + } + + return readBox(); + } + // // @@ -329,7 +411,7 @@ public class Mp4DashReader { return obj; } } - + return obj; } @@ -397,14 +479,14 @@ public class Mp4DashReader { private long parse_tfdt() throws IOException { int version = stream.read(); - stream.skipBytes(3);// flags + stream.skipBytes(3);// flags return version == 0 ? readUint() : stream.readLong(); } private Trun parse_trun() throws IOException { Trun obj = new Trun(); obj.bFlags = stream.readInt(); - obj.entryCount = stream.readInt();// unsigned int + obj.entryCount = stream.readInt();// unsigned int obj.entries_rowSize = 0; if (hasFlag(obj.bFlags, 0x0100)) { @@ -448,11 +530,18 @@ public class Mp4DashReader { return obj; } - private int parse_ftyp() throws IOException { - int brand = stream.readInt(); + private int[] parse_ftyp(Box ref) throws IOException { + 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 - return brand; + for (; i < list.length; i++) + list[i] = stream.readInt();// compatible brands + + return list; } private Mvhd parse_mvhd() throws IOException { @@ -521,32 +610,66 @@ public class Mp4DashReader { trak.tkhd = parse_tkhd(); ensure(b); - b = untilBox(ref, ATOM_MDIA); - trak.mdia = new byte[b.size]; + while ((b = untilBox(ref, ATOM_MDIA, ATOM_EDTS)) != null) { + 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); - buffer.putInt(b.size); - buffer.putInt(ATOM_MDIA); - stream.read(trak.mdia, 8, b.size - 8); - - trak.mdia_mdhd_timeScale = parse_mdia(buffer); + ensure(b); + } return trak; } - private int parse_mdia(ByteBuffer data) { - while (data.hasRemaining()) { - 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(); - } + private Mdia parse_mdia(Box ref) throws IOException { + Mdia obj = new Mdia(); - 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 { @@ -570,7 +693,7 @@ public class Mp4DashReader { ensure(b); } - moov.trak = tmp.toArray(new Trak[tmp.size()]); + moov.trak = tmp.toArray(new Trak[0]); return moov; } @@ -584,7 +707,7 @@ public class Mp4DashReader { ensure(b); } - return tmp.toArray(new Trex[tmp.size()]); + return tmp.toArray(new Trex[0]); } private Trex parse_trex() throws IOException { @@ -602,74 +725,74 @@ public class Mp4DashReader { return obj; } - private Tfra parse_tfra() throws IOException { - int version = stream.read(); - - stream.skipBytes(3);// flags - - 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)); + private Elst parse_edts(Box ref) throws IOException { + Box b = untilBox(ref, ATOM_ELST); + if (b == null) { + return null; } - return tfra; - } - - private Sidx parse_sidx() throws IOException { - int version = stream.read(); + Elst obj = new Elst(); + boolean v1 = stream.read() == 1; stream.skipBytes(3);// flags - Sidx obj = new Sidx(); - obj.referenceId = stream.readInt(); - obj.timescale = stream.readInt(); + int entryCount = stream.readInt(); + if (entryCount < 1) { + obj.bMediaRate = 0x00010000;// default media rate (1.0) + return obj; + } - // earliest presentation entries_time - // first offset - // reserved - stream.skipBytes((2 * (version == 0 ? 4 : 8)) + 2); + if (v1) { + stream.skipBytes(DataReader.LONG_SIZE);// segment duration + obj.MediaTime = stream.readLong(); + // 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++) { - // reference type - // referenced size - stream.skipBytes(4); - obj.entries_subsegmentDuration[i] = stream.readInt();// unsigned int + return obj; + } - // starts with SAP - // SAP type - // SAP delta entries_time - stream.skipBytes(4); + private Minf parse_minf(Box ref) throws IOException { + Minf obj = new Minf(); + + 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; } - private Tfra[] parse_mfra(Box ref, int trackCount) throws IOException { - ArrayList tmp = new ArrayList<>(trackCount); - long limit = ref.offset + ref.size; + /** + * this only read the "stsd" box inside + */ + private byte[] parse_stbl(Box ref) throws IOException { + Box b = untilBox(ref, ATOM_STSD); - while (stream.position() < limit) { - box = readBox(); - - if (box.type == ATOM_TFRA) { - tmp.add(parse_tfra()); - } - - ensure(box); + if (b == null) { + return new byte[0];// this never should happens (missing codec startup data) } - return tmp.toArray(new Tfra[tmp.size()]); + return readFullBox(b); } // @@ -679,14 +802,7 @@ public class Mp4DashReader { int type; long offset; - int size; - } - - class Sidx { - - int timescale; - int referenceId; - int[] entries_subsegmentDuration; + long size; } public class Moof { @@ -711,12 +827,16 @@ public class Mp4DashReader { 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 { @@ -749,6 +869,31 @@ public class Mp4DashReader { 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; } } @@ -768,9 +913,9 @@ public class Mp4DashReader { public class Trak { public Tkhd tkhd; - public int mdia_mdhd_timeScale; + public Elst edst_elst; + public Mdia mdia; - byte[] mdia; } class Mvhd { @@ -786,12 +931,6 @@ public class Mp4DashReader { Trex[] mvex_trex; } - class Tfra { - - int trackId; - int[] entries_time; - } - public class Trex { private int trackId; @@ -801,6 +940,34 @@ public class Mp4DashReader { 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 TrackKind kind; @@ -808,10 +975,43 @@ public class Mp4DashReader { public Trex trex; } - public class Mp4TrackChunk { + public class Mp4DashChunk { public InputStream data; 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; } // } diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4DashWriter.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4DashWriter.java deleted file mode 100644 index babb2e24c..000000000 --- a/app/src/main/java/org/schabi/newpipe/streams/Mp4DashWriter.java +++ /dev/null @@ -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> chunkTimes; - private ArrayList moofOffsets; - private ArrayList 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(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()) { - // - 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); - } - // - } - ArrayList 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; - } - - // - 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); - } - // - - // - private byte[][] make_moof(int sequence, ArrayList 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 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 times, List 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 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 - }); - - } - -// - - class TrunExtra { - - ByteBuffer byteBuffer; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java new file mode 100644 index 000000000..5a4efbe32 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java @@ -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.dispose(); + } + + 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(); + } + + // + 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 + } + } + // + + 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 { + // hard way + 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; + } + } + + // + 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; + } + } + // + + // + 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(); + } + // + + // + 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(); + } + // + + 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; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/streams/SubtitleConverter.java b/app/src/main/java/org/schabi/newpipe/streams/SubtitleConverter.java index 26aaf49a5..c41db4373 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/SubtitleConverter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/SubtitleConverter.java @@ -1,5 +1,6 @@ package org.schabi.newpipe.streams; +import org.schabi.newpipe.streams.io.SharpStream; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; @@ -12,8 +13,6 @@ import java.nio.charset.Charset; import java.text.ParseException; import java.util.Locale; -import org.schabi.newpipe.streams.io.SharpStream; - import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; 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 ) throws IOException, ParseException, SAXException, ParserConfigurationException, XPathExpressionException { - + final FrameWriter callback = new FrameWriter() { int frameIndex = 0; final Charset charset = Charset.forName("utf-8"); - + @Override public void yield(SubtitleFrame frame) throws IOException { if (ignoreEmptyFrames && frame.isEmptyText()) { @@ -48,13 +47,13 @@ public class SubtitleConverter { out.write(NEW_LINE.getBytes(charset)); } }; - + read_xml_based(in, callback, detectYoutubeDuplicateLines, "tt", "xmlns", "http://www.w3.org/ns/ttml", new String[]{"timedtext", "head", "wp"}, new String[]{"body", "div", "p"}, "begin", "end", true - ); + ); } private void read_xml_based(SharpStream source, FrameWriter callback, boolean detectYoutubeDuplicateLines, @@ -70,7 +69,7 @@ public class SubtitleConverter { * Language parsing is not supported */ - byte[] buffer = new byte[source.available()]; + byte[] buffer = new byte[(int) source.available()]; source.read(buffer); 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(); for (int i = 0; i < path.length - 1; i++) { diff --git a/app/src/main/java/org/schabi/newpipe/streams/TrackDataChunk.java b/app/src/main/java/org/schabi/newpipe/streams/TrackDataChunk.java deleted file mode 100644 index 86eb5ff4f..000000000 --- a/app/src/main/java/org/schabi/newpipe/streams/TrackDataChunk.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java b/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java index f61ef14c5..0c635ebe3 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java +++ b/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java @@ -1,12 +1,13 @@ package org.schabi.newpipe.streams; +import org.schabi.newpipe.streams.io.SharpStream; + import java.io.EOFException; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; 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 { - 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 { @@ -193,6 +194,7 @@ public class WebMReader { return elem; } } + ensure(elem); } @@ -306,7 +308,7 @@ public class WebMReader { entry.trackNumber = readNumber(elem); break; case ID_TrackType: - entry.trackType = (int)readNumber(elem); + entry.trackType = (int) readNumber(elem); break; case ID_CodecID: entry.codecId = readString(elem); @@ -445,7 +447,7 @@ public class WebMReader { public class SimpleBlock { - public TrackDataChunk data; + public InputStream data; SimpleBlock(Element ref) { this.ref = ref; @@ -492,7 +494,7 @@ public class WebMReader { currentSimpleBlock = readSimpleBlock(elem); if (currentSimpleBlock.trackNumber == tracks[selectedTrack].trackNumber) { - currentSimpleBlock.data = new TrackDataChunk(stream, (int) currentSimpleBlock.dataSize); + currentSimpleBlock.data = stream.getView((int) currentSimpleBlock.dataSize); return currentSimpleBlock; } diff --git a/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java index ea038c607..eba2bbb87 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java @@ -1,20 +1,20 @@ package org.schabi.newpipe.streams; +import android.support.annotation.NonNull; + import org.schabi.newpipe.streams.WebMReader.Cluster; import org.schabi.newpipe.streams.WebMReader.Segment; import org.schabi.newpipe.streams.WebMReader.SimpleBlock; import org.schabi.newpipe.streams.WebMReader.WebMTrack; +import org.schabi.newpipe.streams.io.SharpStream; import java.io.IOException; import java.io.InputStream; -import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import org.schabi.newpipe.streams.io.SharpStream; - /** - * * @author kapodamy */ public class WebMWriter { @@ -94,10 +94,6 @@ public class WebMWriter { } } - public long getBytesWritten() { - return written; - } - public boolean isDone() { return done; } @@ -138,42 +134,42 @@ public class WebMWriter { /* segment */ listBuffer.add(new byte[]{ - 0x18, 0x53, (byte) 0x80, 0x67, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00// segment content size + 0x18, 0x53, (byte) 0x80, 0x67, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00// segment content size }); long baseSegmentOffset = written + listBuffer.get(0).length; /* seek head */ listBuffer.add(new byte[]{ - 0x11, 0x4d, (byte) 0x9b, 0x74, (byte) 0xbe, - 0x4d, (byte) 0xbb, (byte) 0x8b, - 0x53, (byte) 0xab, (byte) 0x84, 0x15, 0x49, (byte) 0xa9, 0x66, 0x53, - (byte) 0xac, (byte) 0x81, /*info offset*/ 0x43, - 0x4d, (byte) 0xbb, (byte) 0x8b, 0x53, (byte) 0xab, - (byte) 0x84, 0x16, 0x54, (byte) 0xae, 0x6b, 0x53, (byte) 0xac, (byte) 0x81, - /*tracks offset*/ 0x6a, - 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, - 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 + 0x11, 0x4d, (byte) 0x9b, 0x74, (byte) 0xbe, + 0x4d, (byte) 0xbb, (byte) 0x8b, + 0x53, (byte) 0xab, (byte) 0x84, 0x15, 0x49, (byte) 0xa9, 0x66, 0x53, + (byte) 0xac, (byte) 0x81, /*info offset*/ 0x43, + 0x4d, (byte) 0xbb, (byte) 0x8b, 0x53, (byte) 0xab, + (byte) 0x84, 0x16, 0x54, (byte) 0xae, 0x6b, 0x53, (byte) 0xac, (byte) 0x81, + /*tracks offset*/ 0x6a, + 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, + 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 }); /* info */ 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(new byte[]{0x44, (byte) 0x89, (byte) 0x84, - 0x00, 0x00, 0x00, 0x00,// info.duration - - /* MuxingApp */ - 0x4d, (byte) 0x80, (byte) 0x87, 0x4E, - 0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string - - /* WritingApp */ - 0x57, 0x41, (byte) 0x87, 0x4E, - 0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string + 0x00, 0x00, 0x00, 0x00,// info.duration + + /* MuxingApp */ + 0x4d, (byte) 0x80, (byte) 0x87, 0x4E, + 0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string + + /* WritingApp */ + 0x57, 0x41, (byte) 0x87, 0x4E, + 0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string }); /* tracks */ @@ -200,7 +196,6 @@ public class WebMWriter { long nextCueTime = infoTracks[cuesForTrackId].trackType == 1 ? -1 : 0; ArrayList keyFrames = new ArrayList<>(32); - //ArrayList chunks = new ArrayList<>(readers.length); ArrayList clusterOffsets = new ArrayList<>(32); ArrayList clusterSizes = new ArrayList<>(32); @@ -283,24 +278,21 @@ public class WebMWriter { long segmentSize = written - offsetSegmentSizeSet - 7; - // final step write offsets and sizes - out.rewind(); - written = 0; - - skipTo(out, offsetSegmentSizeSet); + /* ---- final step write offsets and sizes ---- */ + seekTo(out, offsetSegmentSizeSet); writeLong(out, segmentSize); if (predefinedDurations[durationFromTrackId] > -1) { duration += predefinedDurations[durationFromTrackId];// this value is full-filled in makeTrackEntry() method } - skipTo(out, offsetInfoDurationSet); + seekTo(out, offsetInfoDurationSet); writeFloat(out, duration); firstClusterOffset -= baseSegmentOffset; - skipTo(out, offsetClusterSet); + seekTo(out, offsetClusterSet); writeInt(out, firstClusterOffset); - skipTo(out, cueReservedOffset); + seekTo(out, cueReservedOffset); /* Cue */ dump(new byte[]{0x1c, 0x53, (byte) 0xbb, 0x6b, 0x20, 0x00, 0x00}, out); @@ -321,17 +313,14 @@ public class WebMWriter { voidBuffer.putShort((short) (firstClusterOffset - written - 4)); dump(voidBuffer.array(), out); - out.rewind(); - written = 0; - - skipTo(out, offsetCuesSet); + seekTo(out, offsetCuesSet); writeInt(out, (int) (cueReservedOffset - baseSegmentOffset)); - skipTo(out, cueReservedOffset + 5); + seekTo(out, cueReservedOffset + 5); writeShort(out, cueSize); 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(); out.write(size, 1, 3); written += 3; @@ -365,20 +354,29 @@ public class WebMWriter { bloq.dataSize = (int) res.dataSize; bloq.trackNumber = internalTrackId; 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; return bloq; } - private short convertTimecode(int time, long oldTimeScale, int newTimeScale) { - return (short) (time * (newTimeScale / oldTimeScale)); + private short convertTimecode(int time, long oldTimeScale) { + return (short) (time * (DEFAULT_TIMECODE_SCALE / oldTimeScale)); } - private void skipTo(SharpStream stream, long absoluteOffset) throws IOException { - absoluteOffset -= written; - written += absoluteOffset; - stream.skip(absoluteOffset); + private void seekTo(SharpStream stream, long offset) throws IOException { + if (stream.canSeek()) { + stream.seek(offset); + } 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 { @@ -468,12 +466,12 @@ public class WebMWriter { private void makeEBML(SharpStream stream) throws IOException { // deafult values dump(new byte[]{ - 0x1A, 0x45, (byte) 0xDF, (byte) 0xA3, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x1F, 0x42, (byte) 0x86, (byte) 0x81, 0x01, - 0x42, (byte) 0xF7, (byte) 0x81, 0x01, 0x42, (byte) 0xF2, (byte) 0x81, 0x04, - 0x42, (byte) 0xF3, (byte) 0x81, 0x08, 0x42, (byte) 0x82, (byte) 0x84, 0x77, - 0x65, 0x62, 0x6D, 0x42, (byte) 0x87, (byte) 0x81, 0x02, - 0x42, (byte) 0x85, (byte) 0x81, 0x02 + 0x1A, 0x45, (byte) 0xDF, (byte) 0xA3, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x1F, 0x42, (byte) 0x86, (byte) 0x81, 0x01, + 0x42, (byte) 0xF7, (byte) 0x81, 0x01, 0x42, (byte) 0xF2, (byte) 0x81, 0x04, + 0x42, (byte) 0xF3, (byte) 0x81, 0x08, 0x42, (byte) 0x82, (byte) 0x84, 0x77, + 0x65, 0x62, 0x6D, 0x42, (byte) 0x87, (byte) 0x81, 0x02, + 0x42, (byte) 0x85, (byte) 0x81, 0x02 }, stream); } @@ -618,9 +616,10 @@ public class WebMWriter { int offset = withLength ? 1 : 0; 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); if (!withLength && i == marker) { b = b | (0x80 >> (length - 1)); @@ -637,11 +636,7 @@ public class WebMWriter { private ArrayList encode(String value) { byte[] str; - try { - str = value.getBytes("utf-8"); - } catch (UnsupportedEncodingException err) { - str = value.getBytes(); - } + str = value.getBytes(StandardCharsets.UTF_8);// or use "utf-8" ArrayList buffer = new ArrayList<>(2); buffer.add(encode(str.length, false)); @@ -720,9 +715,10 @@ public class WebMWriter { return (flags & 0x80) == 0x80; } + @NonNull @Override 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); } } } diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java b/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java index 48bea06f6..ea2f60837 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java +++ b/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java @@ -3,7 +3,7 @@ package org.schabi.newpipe.streams.io; import java.io.IOException; /** - * based c# + * based on c# */ public abstract class SharpStream { @@ -15,23 +15,27 @@ public abstract class SharpStream { public abstract long skip(long amount) throws IOException; - - public abstract int available(); + public abstract long available(); public abstract void rewind() throws IOException; - public abstract void dispose(); public abstract boolean isDisposed(); - public abstract boolean canRewind(); public abstract boolean canRead(); public abstract boolean canWrite(); + public boolean canSetLength() { + return false; + } + + public boolean canSeek() { + return false; + } public abstract void write(byte value) throws IOException; @@ -39,9 +43,15 @@ public abstract class SharpStream { 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 { throw new IOException("Not implemented"); } + + public void seek(long offset) throws IOException { + throw new IOException("Not implemented"); + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java index 8fc423837..fa5530f12 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java @@ -430,24 +430,26 @@ public final class ListHelper { */ private static String getResolutionLimit(Context context) { String resolutionLimit = null; - if (!isWifiActive(context)) { + if (isMeteredNetwork(context)) { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); String defValue = context.getString(R.string.limit_data_usage_none_key); String value = preferences.getString( context.getString(R.string.limit_mobile_data_usage_key), defValue); - resolutionLimit = value.equals(defValue) ? null : value; + resolutionLimit = defValue.equals(value) ? null : value; } return resolutionLimit; } /** - * Are we connected to wifi? + * The current network is metered (like mobile data)? * @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); - return manager != null && manager.getActiveNetworkInfo() != null && manager.getActiveNetworkInfo().getType() == ConnectivityManager.TYPE_WIFI; + if (manager == null || manager.getActiveNetworkInfo() == null) return false; + + return manager.isActiveNetworkMetered(); } } diff --git a/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java b/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java index b3522aea0..7febfa053 100644 --- a/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java @@ -38,7 +38,7 @@ public class SecondaryStreamHelper { public static AudioStream getAudioStreamFor(@NonNull List audioStreams, @NonNull VideoStream videoStream) { switch (videoStream.getFormat()) { case WEBM: - case MPEG_4: + case MPEG_4:// ¿is mpeg-4 DASH? break; default: return null; diff --git a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java index b864cf4fb..abc934878 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java @@ -164,9 +164,6 @@ public class DownloadInitializer extends Thread { } } - // hide marquee in the progress bar - mMission.done++; - mMission.start(); } diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMission.java b/app/src/main/java/us/shandian/giga/get/DownloadMission.java index 243a8585a..b8849482a 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -40,6 +40,9 @@ public class DownloadMission extends Mission { public static final int ERROR_UNKNOWN_HOST = 1005; public static final int ERROR_CONNECT_HOST = 1006; 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_HTTP_NO_CONTENT = 204; public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206; @@ -83,8 +86,9 @@ public class DownloadMission extends Mission { * 0: ready * 1: running * 2: completed + * 3: hold */ - public int postprocessingState; + public volatile int postprocessingState; /** * Indicate if the post-processing algorithm works on the same file @@ -92,19 +96,19 @@ public class DownloadMission extends Mission { public boolean postprocessingThis; /** - * The current resource to download {@code urls[current]} + * The current resource to download, see {@code urls[current]} and {@code offsets[current]} */ public int current; /** * Metadata where the mission state is saved */ - public File metadata; + public transient File metadata; /** * maximum attempts */ - public int maxRetry; + public transient int maxRetry; /** * Approximated final length, this represent the sum of all resources sizes @@ -115,11 +119,11 @@ public class DownloadMission extends Mission { boolean fallback; private int finishCount; public transient boolean running; - public transient boolean enqueued = true; + public boolean enqueued; public int errCode = ERROR_NOTHING; - public transient Exception errObject = null; + public Exception errObject = null; public transient boolean recovered; public transient Handler mHandler; private transient boolean mWritingToFile; @@ -131,7 +135,7 @@ public class DownloadMission extends Mission { private transient boolean deleted; int currentThreadCount; - private transient Thread[] threads = new Thread[0]; + public transient volatile Thread[] threads = new Thread[0]; private transient Thread init = null; @@ -155,6 +159,8 @@ public class DownloadMission extends Mission { this.location = location; this.kind = kind; this.offsets = new long[urls.length]; + this.enqueued = true; + this.maxRetry = 3; if (postprocessingName != null) { Postprocessing algorithm = Postprocessing.getAlgorithm(postprocessingName, null); @@ -183,6 +189,7 @@ public class DownloadMission extends Mission { */ boolean isBlockPreserved(long block) { checkBlock(block); + //noinspection ConstantConditions return blockState.containsKey(block) ? blockState.get(block) : false; } @@ -247,6 +254,12 @@ public class DownloadMission extends Mission { HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setInstanceFollowRedirects(true); + // BUG workaround: switching between networks can freeze the download forever + + //conn.setRequestProperty("Connection", "close"); + conn.setConnectTimeout(30000); + conn.setReadTimeout(10000); + if (rangeStart >= 0) { String req = "bytes=" + rangeStart + "-"; if (rangeEnd > 0) req += rangeEnd; @@ -342,11 +355,32 @@ public class DownloadMission extends Mission { } } - synchronized void notifyError(int code, Exception err) { + public synchronized void notifyError(int code, Exception err) { Log.e(TAG, "notifyError() code = " + code, err); + if (err instanceof IOException) { + if (err.getMessage().contains("Permission denied")) { + code = ERROR_PERMISSION_DENIED; + err = null; + } else if (err.getMessage().contains("write failed: ENOSPC")) { + code = ERROR_INSUFFICIENT_STORAGE; + err = null; + } else { + try { + File storage = new File(location); + if (storage.canWrite() && storage.getUsableSpace() < (getLength() - done)) { + code = ERROR_INSUFFICIENT_STORAGE; + err = null; + } + } catch (SecurityException e) { + // is a permission error + } + } + } + errCode = code; errObject = err; + enqueued = false; pause(); @@ -378,6 +412,7 @@ public class DownloadMission extends Mission { if (!doPostprocessing()) return; + enqueued = false; running = false; deleteThisFromFile(); @@ -386,22 +421,20 @@ public class DownloadMission extends Mission { } private void notifyPostProcessing(int state) { - if (DEBUG) { - String action; - switch (state) { - case 1: - action = "Running"; - break; - case 2: - action = "Completed"; - break; - default: - action = "Failed"; - } - - Log.d(TAG, action + " postprocessing on " + location + File.separator + name); + String action; + switch (state) { + case 1: + action = "Running"; + break; + case 2: + action = "Completed"; + break; + default: + action = "Failed"; } + Log.d(TAG, action + " postprocessing on " + location + File.separator + name); + synchronized (blockState) { // don't return without fully write the current state postprocessingState = state; @@ -420,7 +453,6 @@ public class DownloadMission extends Mission { if (threads != null) for (Thread thread : threads) joinForThread(thread); - enqueued = false; running = true; errCode = ERROR_NOTHING; @@ -463,7 +495,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() { if (!running) return; @@ -477,7 +509,6 @@ public class DownloadMission extends Mission { running = false; recovered = true; - enqueued = false; if (init != null && Thread.currentThread() != init && init.isAlive()) { init.interrupt(); @@ -514,7 +545,7 @@ public class DownloadMission extends Mission { } /** - * Removes the file and the meta file + * Removes the downloaded file and the meta file */ @Override public boolean delete() { @@ -580,12 +611,21 @@ public class DownloadMission extends Mission { * @return true, otherwise, false */ public boolean isPsRunning() { - return postprocessingName != null && postprocessingState == 1; + return postprocessingName != null && (postprocessingState == 1 || postprocessingState == 3); + } + + /** + * Indicated if the mission is ready + * + * @return true, otherwise, false + */ + public boolean isInitialized() { + return blocks >= 0; // DownloadMissionInitializer was executed } public long getLength() { long calculated; - if (postprocessingState == 1) { + if (postprocessingState == 1 || postprocessingState == 3) { calculated = length; } else { calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length; @@ -596,13 +636,37 @@ public class DownloadMission extends Mission { 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) { + postprocessingState = 1; + errCode = recover ? ERROR_NOTHING : ERROR_POSTPROCESSING; + threads[0].interrupt(); + } + private boolean doPostprocessing() { if (postprocessingName == null || postprocessingState == 2) return true; notifyPostProcessing(1); notifyProgress(0); - Thread.currentThread().setName("[" + TAG + "] post-processing = " + postprocessingName + " filename = " + name); + if (DEBUG) + Thread.currentThread().setName("[" + TAG + "] post-processing = " + postprocessingName + " filename = " + name); + + threads = new Thread[]{Thread.currentThread()}; Exception exception = null; diff --git a/app/src/main/java/us/shandian/giga/get/Mission.java b/app/src/main/java/us/shandian/giga/get/Mission.java index ec2ddaa26..53c81b08b 100644 --- a/app/src/main/java/us/shandian/giga/get/Mission.java +++ b/app/src/main/java/us/shandian/giga/get/Mission.java @@ -47,6 +47,11 @@ public abstract class Mission implements Serializable { 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() { deleted = true; return getDownloadedFile().delete(); diff --git a/app/src/main/java/us/shandian/giga/postprocessing/M4aNoDash.java b/app/src/main/java/us/shandian/giga/postprocessing/M4aNoDash.java new file mode 100644 index 000000000..fa0c2c7ae --- /dev/null +++ b/app/src/main/java/us/shandian/giga/postprocessing/M4aNoDash.java @@ -0,0 +1,43 @@ +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; + +import us.shandian.giga.get.DownloadMission; + +public class M4aNoDash extends Postprocessing { + + M4aNoDash(DownloadMission mission) { + super(mission, 0, true); + } + + @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; + } +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Mp4DashMuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java similarity index 60% rename from app/src/main/java/us/shandian/giga/postprocessing/Mp4DashMuxer.java rename to app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java index 45c06dd4b..09f5d9661 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Mp4DashMuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java @@ -1,29 +1,29 @@ -package us.shandian.giga.postprocessing; - -import org.schabi.newpipe.streams.Mp4DashWriter; -import org.schabi.newpipe.streams.io.SharpStream; - -import java.io.IOException; - -import us.shandian.giga.get.DownloadMission; - -/** - * @author kapodamy - */ -class Mp4DashMuxer extends Postprocessing { - - Mp4DashMuxer(DownloadMission mission) { - super(mission, 15360 * 1024/* 15 MiB */, true); - } - - @Override - int process(SharpStream out, SharpStream... sources) throws IOException { - Mp4DashWriter muxer = new Mp4DashWriter(sources); - muxer.parseSources(); - muxer.selectTracks(0, 0); - muxer.build(out); - - return OK_RESULT; - } - -} +package us.shandian.giga.postprocessing; + +import org.schabi.newpipe.streams.Mp4FromDashWriter; +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.IOException; + +import us.shandian.giga.get.DownloadMission; + +/** + * @author kapodamy + */ +class Mp4FromDashMuxer extends Postprocessing { + + Mp4FromDashMuxer(DownloadMission mission) { + super(mission, 2 * 1024 * 1024/* 2 MiB */, true); + } + + @Override + int process(SharpStream out, SharpStream... sources) throws IOException { + Mp4FromDashWriter muxer = new Mp4FromDashWriter(sources); + muxer.parseSources(); + muxer.selectTracks(0, 0); + muxer.build(out); + + return OK_RESULT; + } + +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Mp4Muxer.java b/app/src/main/java/us/shandian/giga/postprocessing/Mp4Muxer.java deleted file mode 100644 index bf932d5c1..000000000 --- a/app/src/main/java/us/shandian/giga/postprocessing/Mp4Muxer.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java index 635140bd3..df8549010 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java @@ -1,6 +1,7 @@ package us.shandian.giga.postprocessing; import android.os.Message; +import android.util.Log; import org.schabi.newpipe.streams.io.SharpStream; @@ -9,17 +10,22 @@ import java.io.IOException; import us.shandian.giga.get.DownloadMission; import us.shandian.giga.postprocessing.io.ChunkFileInputStream; -import us.shandian.giga.postprocessing.io.CircularFile; +import us.shandian.giga.postprocessing.io.CircularFileWriter; +import us.shandian.giga.postprocessing.io.CircularFileWriter.OffsetChecker; import us.shandian.giga.service.DownloadManagerService; +import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING; +import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD; +import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; + public abstract class Postprocessing { - static final byte OK_RESULT = DownloadMission.ERROR_NOTHING; + static final byte OK_RESULT = ERROR_NOTHING; public static final String ALGORITHM_TTML_CONVERTER = "ttml"; - public static final String ALGORITHM_MP4_DASH_MUXER = "mp4D"; - public static final String ALGORITHM_MP4_MUXER = "mp4"; public static final String ALGORITHM_WEBM_MUXER = "webm"; + public static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4"; + public static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a"; public static Postprocessing getAlgorithm(String algorithmName, DownloadMission mission) { if (null == algorithmName) { @@ -27,14 +33,14 @@ public abstract class Postprocessing { } else switch (algorithmName) { case ALGORITHM_TTML_CONVERTER: return new TtmlConverter(mission); - case ALGORITHM_MP4_DASH_MUXER: - return new Mp4DashMuxer(mission); - case ALGORITHM_MP4_MUXER: - return new Mp4Muxer(mission); case ALGORITHM_WEBM_MUXER: return new WebMMuxer(mission); + case ALGORITHM_MP4_FROM_DASH_MUXER: + return new Mp4FromDashMuxer(mission); + case ALGORITHM_M4A_NO_DASH: + return new M4aNoDash(mission); /*case "example-algorithm": - return new ExampleAlgorithm(mission);*/ + return new ExampleAlgorithm(mission);*/ default: throw new RuntimeException("Unimplemented post-processing algorithm: " + algorithmName); } @@ -65,7 +71,8 @@ public abstract class Postprocessing { public void run() throws IOException { File file = mission.getDownloadedFile(); - CircularFile out = null; + File temp = null; + CircularFileWriter out = null; int result; long finalLength = -1; @@ -81,29 +88,54 @@ public abstract class Postprocessing { } sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.getDownloadedFile().length(), "rw"); - int[] idx = {0}; - CircularFile.OffsetChecker checker = () -> { - while (idx[0] < sources.length) { - /* - * WARNING: never use rewind() in any chunk after any writing (especially on first chunks) - * or the CircularFile can lead to unexpected results - */ - if (sources[idx[0]].isDisposed() || sources[idx[0]].available() < 1) { - idx[0]++; - continue;// the selected source is not used anymore + if (test(sources)) { + for (SharpStream source : sources) source.rewind(); + + OffsetChecker checker = () -> { + for (ChunkFileInputStream source : sources) { + /* + * WARNING: never use rewind() in any chunk after any writing (especially on first chunks) + * or the CircularFileWriter can lead to unexpected results + */ + if (source.isDisposed() || source.available() < 1) { + continue;// the selected source is not used anymore + } + + return source.getFilePointer() - 1; } - return sources[idx[0]].getFilePointer() - 1; - } + return -1; + }; - return -1; - }; - out = new CircularFile(file, 0, this::progressReport, checker); + temp = new File(mission.location, mission.name + ".tmp"); - result = process(out, sources); + out = new CircularFileWriter(file, temp, checker); + out.onProgress = this::progressReport; - if (result == OK_RESULT) - finalLength = out.finalizeFile(); + out.onWriteError = (err) -> { + mission.postprocessingState = 3; + mission.notifyError(ERROR_POSTPROCESSING_HOLD, err); + + try { + synchronized (this) { + while (mission.postprocessingState == 3) + wait(); + } + } catch (InterruptedException e) { + // nothing to do + Log.e(this.getClass().getSimpleName(), "got InterruptedException"); + } + + return mission.errCode == ERROR_NOTHING; + }; + + result = process(out, sources); + + if (result == OK_RESULT) + finalLength = out.finalizeFile(); + } else { + result = OK_RESULT; + } } finally { for (SharpStream source : sources) { if (source != null && !source.isDisposed()) { @@ -113,17 +145,22 @@ public abstract class Postprocessing { if (out != null) { out.dispose(); } + if (temp != null) { + //noinspection ResultOfMethodCallIgnored + temp.delete(); + } } } else { - result = process(null); + result = test() ? process(null) : OK_RESULT; } if (result == OK_RESULT) { - if (finalLength < 0) finalLength = file.length(); - mission.done = finalLength; - mission.length = finalLength; + if (finalLength != -1) { + mission.done = finalLength; + mission.length = finalLength; + } } else { - mission.errCode = DownloadMission.ERROR_UNKNOWN_EXCEPTION; + mission.errCode = ERROR_UNKNOWN_EXCEPTION; mission.errObject = new RuntimeException("post-processing algorithm returned " + result); } @@ -134,7 +171,18 @@ public abstract class Postprocessing { } /** - * Abstract method to execute the pos-processing algorithm + * 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 @@ -151,7 +199,7 @@ public abstract class Postprocessing { return mission.postprocessingArgs[index]; } - void progressReport(long done) { + private void progressReport(long done) { mission.done = done; if (mission.length < mission.done) mission.length = mission.done; diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/ChunkFileInputStream.java b/app/src/main/java/us/shandian/giga/postprocessing/io/ChunkFileInputStream.java index cd62c5d22..ee2fcddd5 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/io/ChunkFileInputStream.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/io/ChunkFileInputStream.java @@ -94,7 +94,7 @@ public class ChunkFileInputStream extends SharpStream { } @Override - public int available() { + public long available() { return (int) (length - position); } @@ -147,7 +147,4 @@ public class ChunkFileInputStream extends SharpStream { public void write(byte[] buffer, int offset, int count) { } - @Override - public void flush() { - } } diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java b/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java deleted file mode 100644 index d2fc82d33..000000000 --- a/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java +++ /dev/null @@ -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 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; - } - - // - @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"); - } -// - - 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()); - } - - } -} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFileWriter.java b/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFileWriter.java new file mode 100644 index 000000000..4c4160fa3 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFileWriter.java @@ -0,0 +1,459 @@ +package us.shandian.giga.postprocessing.io; + +import android.support.annotation.NonNull; + +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; + +public class CircularFileWriter extends SharpStream { + + private final static int QUEUE_BUFFER_SIZE = 8 * 1024;// 8 KiB + private final static int NOTIFY_BYTES_INTERVAL = 64 * 1024;// 64 KiB + private final static int THRESHOLD_AUX_LENGTH = 3 * 1024 * 1024;// 3 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(File source, 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(source); + + callback = checker; + + reportPosition = NOTIFY_BYTES_INTERVAL; + } + + private void flushAuxiliar() throws IOException { + if (aux.length < 1) { + return; + } + + boolean underflow = out.getOffset() >= out.length; + + out.flush(); + aux.flush(); + + aux.target.seek(0); + out.target.seek(out.length); + + long length = aux.length; + out.length += aux.length; + + while (length > 0) { + int read = (int) Math.min(length, Integer.MAX_VALUE); + read = aux.target.read(aux.queue, 0, Math.min(read, aux.queue.length)); + + out.writeProof(aux.queue, read); + length -= read; + } + + if (underflow) { + out.offset += aux.offset; + out.target.seek(out.offset); + } else { + out.offset = out.length; + } + + if (out.length > maxLengthKnown) { + maxLengthKnown = out.length; + } + + 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(); + + out.flush(); + + // change file length (if required) + long length = Math.max(maxLengthKnown, out.length); + if (length != out.target.length()) { + out.target.setLength(length); + } + + dispose(); + + return length; + } + + /** + * Close the file without flushing any buffer + */ + @Override + public void dispose() { + if (out != null) { + out.dispose(); + out = null; + } + if (aux != null) { + aux.dispose(); + 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: " + String.valueOf(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; + } + + if (length > available || length < THRESHOLD_AUX_LENGTH) { + aux.write(b, off, len); + } else { + if (underflow) { + aux.write(b, off, len); + flushAuxiliar(); + } else { + flushAuxiliar(); + out.write(b, off, len);// write directly on the output + } + } + } 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) { + return;// nothing to do + } + + // 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 isDisposed() { + return out == null; + } + + @Override + public boolean canRewind() { + return true; + } + + @Override + public boolean canWrite() { + return true; + } + + @Override + public boolean canSeek() { + return true; + } + + // + @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"); + } + // + + 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 RandomAccessFile target; + + private long offset; + protected long length; + + private byte[] queue; + private int queueSize; + + BufferedFile(File file) throws FileNotFoundException { + queue = new byte[QUEUE_BUFFER_SIZE]; + target = new RandomAccessFile(file, "rw"); + } + + protected long getOffset() { + return offset + queueSize;// absolute offset in the file + } + + protected void dispose() { + try { + queue = null; + target.close(); + } catch (IOException e) { + // nothing to do + } + } + + 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 + } + } + + protected 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; + } + + protected void reset() throws IOException { + offset = 0; + length = 0; + target.seek(0); + } + + protected void seek(long absoluteOffset) throws IOException { + offset = absoluteOffset; + target.seek(absoluteOffset); + } + + protected 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 absOffset; + String absLength; + + try { + absOffset = Long.toString(target.getFilePointer()); + } catch (IOException e) { + absOffset = "[" + e.getLocalizedMessage() + "]"; + } + try { + absLength = Long.toString(target.length()); + } catch (IOException e) { + absLength = "[" + e.getLocalizedMessage() + "]"; + } + + return String.format( + "offset=%s length=%s queue=%s absOffset=%s absLength=%s", + offset, length, queueSize, absOffset, absLength + ); + } + } +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/FileStream.java b/app/src/main/java/us/shandian/giga/postprocessing/io/FileStream.java deleted file mode 100644 index c1b675eef..000000000 --- a/app/src/main/java/us/shandian/giga/postprocessing/io/FileStream.java +++ /dev/null @@ -1,126 +0,0 @@ -package us.shandian.giga.postprocessing.io; - -import org.schabi.newpipe.streams.io.SharpStream; - -import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.channels.FileChannel; - -/** - * @author kapodamy - */ -public class FileStream extends SharpStream { - - public enum Mode { - Read, - ReadWrite - } - - public RandomAccessFile source; - private final Mode mode; - - public FileStream(String path, Mode mode) throws IOException { - String flags; - - if (mode == Mode.Read) { - flags = "r"; - } else { - flags = "rw"; - } - - this.mode = mode; - source = new RandomAccessFile(path, flags); - } - - @Override - public int read() throws IOException { - return source.read(); - } - - @Override - public int read(byte b[]) throws IOException { - return read(b, 0, b.length); - } - - @Override - public int read(byte b[], int off, int len) throws IOException { - return source.read(b, off, len); - } - - @Override - public long skip(long pos) throws IOException { - FileChannel fc = source.getChannel(); - fc.position(fc.position() + pos); - return pos; - } - - @Override - public int available() { - try { - return (int) (source.length() - source.getFilePointer()); - } catch (IOException ex) { - return 0; - } - } - - @SuppressWarnings("EmptyCatchBlock") - @Override - public void dispose() { - try { - source.close(); - } catch (IOException err) { - - } finally { - source = null; - } - } - - @Override - public boolean isDisposed() { - return source == null; - } - - @Override - public void rewind() throws IOException { - source.getChannel().position(0); - } - - @Override - public boolean canRewind() { - return true; - } - - @Override - public boolean canRead() { - return mode == Mode.Read || mode == Mode.ReadWrite; - } - - @Override - public boolean canWrite() { - return mode == Mode.ReadWrite; - } - - @Override - public void write(byte value) throws IOException { - source.write(value); - } - - @Override - public void write(byte[] buffer) throws IOException { - source.write(buffer); - } - - @Override - public void write(byte[] buffer, int offset, int count) throws IOException { - source.write(buffer, offset, count); - } - - @Override - public void flush() { - } - - @Override - public void setLength(long length) throws IOException { - source.setLength(length); - } -} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/SharpInputStream.java b/app/src/main/java/us/shandian/giga/postprocessing/io/SharpInputStream.java index 52e0775da..586456d98 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/io/SharpInputStream.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/io/SharpInputStream.java @@ -14,6 +14,7 @@ import java.io.InputStream; /** * Wrapper for the classic {@link java.io.InputStream} + * * @author kapodamy */ public class SharpInputStream extends InputStream { @@ -49,7 +50,8 @@ public class SharpInputStream extends InputStream { @Override public int available() { - return base.available(); + long res = base.available(); + return res > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) res; } @Override diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java index 883c26850..58246beb1 100644 --- a/app/src/main/java/us/shandian/giga/service/DownloadManager.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java @@ -21,6 +21,8 @@ import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.FinishedMission; import us.shandian.giga.get.Mission; import us.shandian.giga.get.sqlite.DownloadDataSource; +import us.shandian.giga.service.DownloadManagerService.DMChecker; +import us.shandian.giga.service.DownloadManagerService.MissionCheck; import us.shandian.giga.util.Utility; import static org.schabi.newpipe.BuildConfig.DEBUG; @@ -28,7 +30,7 @@ import static org.schabi.newpipe.BuildConfig.DEBUG; public class DownloadManager { 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_PENDING = 1; @@ -45,7 +47,9 @@ public class DownloadManager { private NetworkState mLastNetworkStatus = NetworkState.Unavailable; int mPrefMaxRetry; - boolean mPrefCrossNetwork; + boolean mPrefMeteredDownloads; + boolean mPrefQueueLimit; + private boolean mSelfMissionsControl; /** * Create a new instance @@ -152,8 +156,8 @@ public class DownloadManager { } mis.postprocessingState = 0; - mis.errCode = DownloadMission.ERROR_POSTPROCESSING; - mis.errObject = new RuntimeException("stopped unexpectedly"); + mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED; + mis.errObject = null; } else if (exists && !dl.isFile()) { // probably a folder, this should never happens if (!sub.delete()) { @@ -162,20 +166,21 @@ public class DownloadManager { continue; } - if (!exists) { + if (!exists && mis.isInitialized()) { // 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; + m.setEnqueued(mis.enqueued); mis = m; } mis.running = false; mis.recovered = exists; mis.metadata = sub; + mis.maxRetry = mPrefMaxRetry; mis.mHandler = mHandler; mMissionsPending.add(mis); @@ -205,17 +210,25 @@ public class DownloadManager { 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); + if (pendingMission.running) { + // generate unique filename (?) + try { + name = generateUniqueName(location, name); + } catch (Exception e) { + Log.e(TAG, "Unable to generate unique name", e); + name = System.currentTimeMillis() + name; + Log.i(TAG, "Using " + name); + } + } else { + // dispose the mission + mMissionsPending.remove(pendingMission); + mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED); + pendingMission.delete(); } } else { - // check for existing finished download + // check for existing finished download and dispose (if exists) int index = getFinishedMissionIndex(location, name); if (index >= 0) mDownloadDataSource.deleteMission(mMissionsFinished.remove(index)); } @@ -242,14 +255,17 @@ public class DownloadManager { mission.timestamp = System.currentTimeMillis(); } + mSelfMissionsControl = true; 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); - if (canDownloadInCurrentNetwork() && (getRunningMissionsCount() < 1)) { + boolean start = !mPrefQueueLimit || getRunningMissionsCount() < 1; + + if (canDownloadInCurrentNetwork() && start) { + mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS); mission.start(); - mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_RUNNING); } } } @@ -257,13 +273,14 @@ public class DownloadManager { public void resumeMission(DownloadMission mission) { if (!mission.running) { + mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS); mission.start(); - mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_RUNNING); } } public void pauseMission(DownloadMission mission) { if (mission.running) { + mission.setEnqueued(false); mission.pause(); mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED); } @@ -335,7 +352,7 @@ public class DownloadManager { int count = 0; synchronized (this) { for (DownloadMission mission : mMissionsPending) { - if (mission.running && !mission.isFinished() && !mission.isPsFailed()) + if (mission.running && !mission.isPsFailed() && !mission.isFinished()) count++; } } @@ -343,10 +360,36 @@ public class DownloadManager { return count; } - void pauseAllMissions() { + public void pauseAllMissions(boolean force) { + boolean flag = false; + 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(); + flag = true; + } } + + if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED); + } + + public void startAllMissions() { + boolean flag = false; + + synchronized (this) { + for (DownloadMission mission : mMissionsPending) { + if (mission.running || mission.isPsFailed() || mission.isFinished()) continue; + + flag = true; + mission.start(); + } + } + + if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS); } @@ -415,31 +458,35 @@ public class DownloadManager { } /** - * 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) { if (mMissionsPending.size() < 1) return false; - - int i = getRunningMissionsCount(); - if (i > 0) return true; - if (!canDownloadInCurrentNetwork()) return false; - for (DownloadMission mission : mMissionsPending) { - if (!mission.running && mission.errCode == DownloadMission.ERROR_NOTHING && mission.enqueued) { - resumeMission(mission); - return true; - } + if (mPrefQueueLimit) { + for (DownloadMission mission : mMissionsPending) + if (!mission.isFinished() && mission.running) return true; } - return false; + boolean flag = false; + for (DownloadMission mission : mMissionsPending) { + if (mission.running || !mission.enqueued || mission.isFinished()) continue; + + resumeMission(mission); + if (mPrefQueueLimit) return true; + flag = true; + } + + return flag; } } public MissionIterator getIterator() { + mSelfMissionsControl = true; return new MissionIterator(); } @@ -457,31 +504,43 @@ public class DownloadManager { private boolean canDownloadInCurrentNetwork() { 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; mLastNetworkStatus = currentStatus; + if (currentStatus == NetworkState.Unavailable) return; - if (currentStatus == NetworkState.Unavailable) { - return; - } else if (currentStatus != NetworkState.MobileOperating || !mPrefCrossNetwork) { - return; + if (!mSelfMissionsControl || updateOnly) { + return;// don't touch anything without the user interaction } - boolean flag = false; + boolean isMetered = mPrefMeteredDownloads && mLastNetworkStatus == NetworkState.MeteredOperating; + + int running = 0; + int paused = 0; synchronized (this) { for (DownloadMission mission : mMissionsPending) { - if (mission.running && !mission.isFinished() && !mission.isPsRunning()) { - flag = true; + if (mission.isFinished() || mission.isPsRunning()) continue; + + if (mission.running && isMetered) { + paused++; 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() { @@ -506,21 +565,24 @@ public class DownloadManager { ), Toast.LENGTH_LONG).show(); } - void checkForRunningMission(String location, String name, DownloadManagerService.DMChecker check) { - boolean listed; - boolean finished = false; + void checkForRunningMission(String location, String name, DMChecker check) { + MissionCheck result = MissionCheck.None; synchronized (this) { - DownloadMission mission = getPendingMission(location, name); - if (mission != null) { - listed = true; + DownloadMission pending = getPendingMission(location, name); + + if (pending == null) { + if (getFinishedMissionIndex(location, name) >= 0) result = MissionCheck.Finished; } else { - listed = getFinishedMissionIndex(location, name) >= 0; - finished = listed; + if (pending.isFinished()) { + result = MissionCheck.Finished;// this never should happen (race-condition) + } else { + result = pending.running ? MissionCheck.PendingRunning : MissionCheck.Pending; + } } } - check.callback(listed, finished); + check.callback(result); } public class MissionIterator extends DiffUtil.Callback { @@ -592,39 +654,6 @@ public class DownloadManager { return SPECIAL_NOTHING; } - public MissionItem getItemUnsafe(int position) { - synchronized (DownloadManager.this) { - int count = mMissionsPending.size(); - int count2 = mMissionsFinished.size(); - - if (count > 0) { - position--; - if (position == -1) - return new MissionItem(SPECIAL_PENDING); - else if (position < count) - return new MissionItem(SPECIAL_NOTHING, mMissionsPending.get(position)); - else if (position == count && count2 > 0) - return new MissionItem(SPECIAL_FINISHED); - else - position -= count; - } else { - if (count2 > 0 && position == 0) { - return new MissionItem(SPECIAL_FINISHED); - } - } - - position--; - - if (count2 < 1) { - throw new RuntimeException( - String.format("Out of range. pending_count=%s finished_count=%s position=%s", count, count2, position) - ); - } - - return new MissionItem(SPECIAL_NOTHING, mMissionsFinished.get(position)); - } - } - public void start() { current = getSpecialItems(); @@ -647,6 +676,32 @@ public class DownloadManager { 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.isPsFailed() || mission.isFinished()) + continue; + + if (mission.running) + paused = true; + else + running = true; + } + } + + return new boolean[]{running, paused}; + } + @Override public int getOldListSize() { diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index a57fe1734..be1e20dd6 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -15,7 +15,9 @@ import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.ConnectivityManager; +import android.net.Network; import android.net.NetworkInfo; +import android.net.NetworkRequest; import android.net.Uri; import android.os.Binder; import android.os.Build; @@ -24,6 +26,7 @@ import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.preference.PreferenceManager; +import android.support.annotation.NonNull; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat.Builder; import android.support.v4.content.PermissionChecker; @@ -48,7 +51,6 @@ public class DownloadManagerService extends Service { private static final String TAG = "DownloadManagerService"; - public static final int MESSAGE_RUNNING = 0; public static final int MESSAGE_PAUSED = 1; public static final int MESSAGE_FINISHED = 2; public static final int MESSAGE_PROGRESS = 3; @@ -76,7 +78,7 @@ public class DownloadManagerService extends Service { private Notification mNotification; private Handler mHandler; private boolean mForeground = false; - private NotificationManager notificationManager = null; + private NotificationManager mNotificationManager = null; private boolean mDownloadNotificationEnable = true; private int downloadDoneCount = 0; @@ -85,7 +87,9 @@ public class DownloadManagerService extends Service { private final ArrayList mEchoObservers = new ArrayList<>(1); - private BroadcastReceiver mNetworkStateListener; + private ConnectivityManager mConnectivityManager; + private BroadcastReceiver mNetworkStateListener = null; + private ConnectivityManager.NetworkCallback mNetworkStateListenerL = null; private SharedPreferences mPrefs = null; private final SharedPreferences.OnSharedPreferenceChangeListener mPrefChangeListener = this::handlePreferenceChange; @@ -147,25 +151,39 @@ public class DownloadManagerService extends Service { .setContentText(getString(R.string.msg_running_detail)); mNotification = builder.build(); - notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - mNetworkStateListener = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false)) { - handleConnectivityChange(null); - return; + mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + mConnectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + mNetworkStateListenerL = new ConnectivityManager.NetworkCallback() { + @Override + public void onAvailable(Network network) { + handleConnectivityState(false); } - handleConnectivityChange(intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO)); - } - }; - registerReceiver(mNetworkStateListener, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); + + @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 = PreferenceManager.getDefaultSharedPreferences(this); mPrefs.registerOnSharedPreferenceChangeListener(mPrefChangeListener); handlePreferenceChange(mPrefs, getString(R.string.downloads_cross_network)); handlePreferenceChange(mPrefs, getString(R.string.downloads_maximum_retry)); + handlePreferenceChange(mPrefs, getString(R.string.downloads_queue_limit)); mLock = new LockManager(this); } @@ -173,12 +191,11 @@ public class DownloadManagerService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { if (DEBUG) { - if (intent == null) { - Log.d(TAG, "Restarting"); - return START_NOT_STICKY; - } - Log.d(TAG, "Starting"); + Log.d(TAG, intent == null ? "Restarting" : "Starting"); } + + if (intent == null) return START_NOT_STICKY; + Log.i(TAG, "Got intent: " + intent); String action = intent.getAction(); if (action != null) { @@ -193,6 +210,8 @@ public class DownloadManagerService extends Service { String source = intent.getStringExtra(EXTRA_SOURCE); long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0); + handleConnectivityState(true);// first check the actual network status + mHandler.post(() -> mManager.startMission(urls, location, name, kind, threads, source, psName, psArgs, nearLength)); } else if (downloadDoneNotification != null) { @@ -221,21 +240,25 @@ public class DownloadManagerService extends Service { 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 - notificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build()); + mNotificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build()); } - mManager.pauseAllMissions(); - manageLock(false); - unregisterReceiver(mNetworkStateListener); + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + mConnectivityManager.unregisterNetworkCallback(mNetworkStateListenerL); + else + unregisterReceiver(mNetworkStateListener); + mPrefs.unregisterOnSharedPreferenceChangeListener(mPrefChangeListener); if (icDownloadDone != null) icDownloadDone.recycle(); if (icDownloadFailed != null) icDownloadFailed.recycle(); if (icLauncher != null) icLauncher.recycle(); + + mManager.pauseAllMissions(true); } @Override @@ -264,15 +287,16 @@ public class DownloadManagerService extends Service { notifyMediaScanner(mission.getDownloadedFile()); notifyFinishedDownload(mission.name); mManager.setFinished(mission); - updateForegroundState(mManager.runAnotherMission()); + handleConnectivityState(false); + updateForegroundState(mManager.runMissions()); break; - case MESSAGE_RUNNING: case MESSAGE_PROGRESS: updateForegroundState(true); break; case MESSAGE_ERROR: notifyFailedDownload(mission); - updateForegroundState(mManager.runAnotherMission()); + handleConnectivityState(false); + updateForegroundState(mManager.runMissions()); break; case MESSAGE_PAUSED: updateForegroundState(mManager.getRunningMissionsCount() > 0); @@ -293,36 +317,30 @@ public class DownloadManagerService extends Service { } } - private void handleConnectivityChange(NetworkInfo info) { + private void handleConnectivityState(boolean updateOnly) { + NetworkInfo info = mConnectivityManager.getActiveNetworkInfo(); NetworkState status; if (info == null) { status = NetworkState.Unavailable; - Log.i(TAG, "actual connectivity status is unavailable"); - } else if (!info.isAvailable() || !info.isConnected()) { - status = NetworkState.Unavailable; - Log.i(TAG, "actual connectivity status is not available and not connected"); + Log.i(TAG, "Active network [connectivity is unavailable]"); } else { - int type = info.getType(); - if (type == ConnectivityManager.TYPE_MOBILE || type == ConnectivityManager.TYPE_MOBILE_DUN) { - status = NetworkState.MobileOperating; - } else if (type == ConnectivityManager.TYPE_WIFI) { - status = NetworkState.WifiOperating; - } else if (type == ConnectivityManager.TYPE_WIMAX || - type == ConnectivityManager.TYPE_ETHERNET || - type == ConnectivityManager.TYPE_BLUETOOTH) { - status = NetworkState.OtherOperating; - } else { + boolean connected = info.isConnected(); + boolean metered = mConnectivityManager.isActiveNetworkMetered(); + + if (connected) + status = metered ? NetworkState.MeteredOperating : NetworkState.Operating; + else 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 - 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))) { try { String value = prefs.getString(key, getString(R.string.downloads_maximum_retry_default)); @@ -332,7 +350,9 @@ public class DownloadManagerService extends Service { } mManager.updateMaximumAttempts(); } 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); } } @@ -366,19 +386,20 @@ public class DownloadManagerService extends Service { context.startService(intent); } - public static void checkForRunningMission(Context context, String location, String name, DMChecker check) { + public static void checkForRunningMission(Context context, String location, String name, DMChecker checker) { Intent intent = new Intent(); intent.setClass(context, DownloadManagerService.class); + context.startService(intent); + context.bindService(intent, new ServiceConnection() { @Override public void onServiceConnected(ComponentName cname, IBinder service) { try { - ((DMBinder) service).getDownloadManager().checkForRunningMission(location, name, check); + ((DMBinder) service).getDownloadManager().checkForRunningMission(location, name, checker); } catch (Exception err) { Log.w(TAG, "checkForRunningMission() callback is defective", err); } - // TODO: find a efficient way to unbind the service. This destroy the service due idle, but is started again when the user start a download. context.unbindService(this); } @@ -389,7 +410,7 @@ public class DownloadManagerService extends Service { } public void notifyFinishedDownload(String name) { - if (!mDownloadNotificationEnable || notificationManager == null) { + if (!mDownloadNotificationEnable || mNotificationManager == null) { return; } @@ -428,7 +449,7 @@ public class DownloadManagerService extends Service { downloadDoneNotification.setContentText(downloadDoneList); } - notificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build()); + mNotificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build()); downloadDoneCount++; } @@ -458,7 +479,7 @@ public class DownloadManagerService extends Service { .bigText(mission.name)); } - notificationManager.notify(id, downloadFailedNotification.build()); + mNotificationManager.notify(id, downloadFailedNotification.build()); } private PendingIntent makePendingIntent(String action) { @@ -487,7 +508,11 @@ public class DownloadManagerService extends Service { mLockAcquired = acquire; } - // Wrapper of DownloadManager + + //////////////////////////////////////////////////////////////////////////////////////////////// + // Wrappers for DownloadManager + //////////////////////////////////////////////////////////////////////////////////////////////// + public class DMBinder extends Binder { public DownloadManager getDownloadManager() { return mManager; @@ -502,15 +527,15 @@ public class DownloadManagerService extends Service { } public void clearDownloadNotifications() { - if (notificationManager == null) return; + if (mNotificationManager == null) return; if (downloadDoneNotification != null) { - notificationManager.cancel(DOWNLOADS_NOTIFICATION_ID); + mNotificationManager.cancel(DOWNLOADS_NOTIFICATION_ID); downloadDoneList.setLength(0); downloadDoneCount = 0; } if (downloadFailedNotification != null) { for (; downloadFailedNotificationID > DOWNLOADS_NOTIFICATION_ID; downloadFailedNotificationID--) { - notificationManager.cancel(downloadFailedNotificationID); + mNotificationManager.cancel(downloadFailedNotificationID); } mFailedDownloads.clear(); downloadFailedNotificationID++; @@ -524,7 +549,9 @@ public class DownloadManagerService extends Service { } public interface DMChecker { - void callback(boolean listed, boolean finished); + void callback(MissionCheck result); } + public enum MissionCheck {None, Pending, PendingRunning, Finished} + } diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index 4a35aa166..cada3aeb8 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -14,15 +14,17 @@ import android.os.Looper; import android.os.Message; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.StringRes; import android.support.v4.content.FileProvider; import android.support.v4.view.ViewCompat; import android.support.v7.app.AlertDialog; import android.support.v7.util.DiffUtil; import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.RecyclerView.ViewHolder; import android.support.v7.widget.RecyclerView.Adapter; +import android.support.v7.widget.RecyclerView.ViewHolder; import android.util.Log; import android.util.SparseArray; +import android.view.HapticFeedbackConstants; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -36,14 +38,17 @@ import android.widget.Toast; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; +import org.schabi.newpipe.report.ErrorActivity; +import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.NavigationHelper; -import java.io.File; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.Collections; import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.FinishedMission; +import us.shandian.giga.get.Mission; import us.shandian.giga.service.DownloadManager; import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.ui.common.Deleter; @@ -57,10 +62,13 @@ 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_HTTP_NO_CONTENT; 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_PATH_CREATION; 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_HOLD; +import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_STOPPED; import static us.shandian.giga.get.DownloadMission.ERROR_SSL_EXCEPTION; import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST; @@ -69,6 +77,7 @@ public class MissionAdapter extends Adapter { private static final SparseArray ALGORITHMS = new SparseArray<>(); private static final String TAG = "MissionAdapter"; private static final String UNDEFINED_PROGRESS = "--.-%"; + private static final String DEFAULT_MIME_TYPE = "*/*"; static { @@ -85,9 +94,11 @@ public class MissionAdapter extends Adapter { private ArrayList mPendingDownloadsItems = new ArrayList<>(); private Handler mHandler; private MenuItem mClear; + private MenuItem mStartButton; + private MenuItem mPauseButton; private View mEmptyMessage; - public MissionAdapter(Context context, DownloadManager downloadManager, MenuItem clearButton, View emptyMessage) { + public MissionAdapter(Context context, DownloadManager downloadManager, View emptyMessage) { mContext = context; mDownloadManager = downloadManager; mDeleter = null; @@ -105,10 +116,18 @@ public class MissionAdapter extends Adapter { onServiceMessage(msg); 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; mIterator = downloadManager.getIterator(); @@ -225,8 +244,10 @@ public class MissionAdapter extends Adapter { long deltaDone = mission.done - h.lastDone; boolean hasError = mission.errCode != ERROR_NOTHING; - // on error hide marquee or show if condition (mission.done < 1 || mission.unknownLength) is true - h.progress.setMarquee(!hasError && (mission.done < 1 || mission.unknownLength)); + // hide on error + // show if current resource length is not fetched + // show if length is unknown + h.progress.setMarquee(!hasError && (!mission.isInitialized() || mission.unknownLength)); float progress; if (mission.unknownLength) { @@ -305,36 +326,64 @@ public class MissionAdapter extends Adapter { } } - private boolean viewWithFileProvider(@NonNull File file) { - if (!file.exists()) return true; + private void viewWithFileProvider(Mission mission) { + if (checkInvalidFile(mission)) return; - String ext = Utility.getFileExt(file.getName()); - if (ext == null) return false; + String mimeType = resolveMimeType(mission); - String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1)); - Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider"); + if (BuildConfig.DEBUG) + Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider"); - Uri uri = FileProvider.getUriForFile(mContext, BuildConfig.APPLICATION_ID + ".provider", file); + Uri uri = FileProvider.getUriForFile( + mContext, + BuildConfig.APPLICATION_ID + ".provider", + mission.getDownloadedFile() + ); Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); intent.setDataAndType(uri, mimeType); intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { intent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION); } - if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { intent.addFlags(FLAG_ACTIVITY_NEW_TASK); } + //mContext.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); - Log.v(TAG, "Starting intent: " + intent); + if (intent.resolveActivity(mContext.getPackageManager()) != null) { mContext.startActivity(intent); } else { - Toast noPlayerToast = Toast.makeText(mContext, R.string.toast_no_player, Toast.LENGTH_LONG); - noPlayerToast.show(); + Toast.makeText(mContext, R.string.toast_no_player, Toast.LENGTH_LONG).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.getDownloadedFile().toURI()); + + mContext.startActivity(Intent.createChooser(intent, null)); + } + + private static String resolveMimeType(@NonNull Mission mission) { + String ext = Utility.getFileExt(mission.getDownloadedFile().getName()); + if (ext == null) return DEFAULT_MIME_TYPE; + + String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1)); + + return mimeType == null ? DEFAULT_MIME_TYPE : mimeType; + } + + private boolean checkInvalidFile(@NonNull Mission mission) { + if (mission.getDownloadedFile().exists()) return false; + + Toast.makeText(mContext, R.string.missing_file, Toast.LENGTH_SHORT).show(); return true; } @@ -343,15 +392,9 @@ public class MissionAdapter extends Adapter { } private void onServiceMessage(@NonNull Message msg) { - switch (msg.what) { - case DownloadManagerService.MESSAGE_PROGRESS: - setAutoRefresh(true); - return; - case DownloadManagerService.MESSAGE_ERROR: - case DownloadManagerService.MESSAGE_FINISHED: - break; - default: - return; + if (msg.what == DownloadManagerService.MESSAGE_PROGRESS) { + setAutoRefresh(true); + return; } for (int i = 0; i < mPendingDownloadsItems.size(); i++) { @@ -370,74 +413,98 @@ public class MissionAdapter extends Adapter { } private void showError(@NonNull DownloadMission mission) { - StringBuilder str = new StringBuilder(); - str.append(mContext.getString(R.string.label_code)); - str.append(": "); - str.append(mission.errCode); - str.append('\n'); + @StringRes int msg = R.string.general_error; + String msgEx = null; switch (mission.errCode) { case 416: - str.append(mContext.getString(R.string.error_http_requested_range_not_satisfiable)); + msg = R.string.error_http_requested_range_not_satisfiable; break; case 404: - str.append(mContext.getString(R.string.error_http_not_found)); + msg = R.string.error_http_not_found; break; case ERROR_NOTHING: - str.append("¿?"); - break; + return;// this never should happen case ERROR_FILE_CREATION: - str.append(mContext.getString(R.string.error_file_creation)); + msg = R.string.error_file_creation; break; case ERROR_HTTP_NO_CONTENT: - str.append(mContext.getString(R.string.error_http_no_content)); + msg = R.string.error_http_no_content; break; case ERROR_HTTP_UNSUPPORTED_RANGE: - str.append(mContext.getString(R.string.error_http_unsupported_range)); + msg = R.string.error_http_unsupported_range; break; case ERROR_PATH_CREATION: - str.append(mContext.getString(R.string.error_path_creation)); + msg = R.string.error_path_creation; break; case ERROR_PERMISSION_DENIED: - str.append(mContext.getString(R.string.permission_denied)); + msg = R.string.permission_denied; break; case ERROR_SSL_EXCEPTION: - str.append(mContext.getString(R.string.error_ssl_exception)); + msg = R.string.error_ssl_exception; break; case ERROR_UNKNOWN_HOST: - str.append(mContext.getString(R.string.error_unknown_host)); + msg = R.string.error_unknown_host; break; 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; case ERROR_POSTPROCESSING: - str.append(mContext.getString(R.string.error_postprocessing_failed)); - case ERROR_UNKNOWN_EXCEPTION: + 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: + showError(mission.errObject, UserAction.DOWNLOAD_FAILED, R.string.general_error); + return; default: if (mission.errCode >= 100 && mission.errCode < 600) { - str = new StringBuilder(8); - str.append("HTTP "); - str.append(mission.errCode); + msgEx = "HTTP " + mission.errCode; } 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; } - if (mission.errObject != null) { - str.append("\n\n"); - str.append(mission.errObject.toString()); + AlertDialog.Builder builder = new AlertDialog.Builder(mContext); + + 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.setTitle(mission.name) - .setMessage(str) - .setNegativeButton(android.R.string.ok, (dialog, which) -> dialog.cancel()) + builder.setNegativeButton(android.R.string.ok, (dialog, which) -> dialog.cancel()) + .setTitle(mission.name) .create() .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() { mDownloadManager.forgetFinishedDownloads(); applyChanges(); @@ -466,16 +533,24 @@ public class MissionAdapter extends Adapter { showError(mission); return true; case R.id.queue: - h.queue.setChecked(!h.queue.isChecked()); - mission.enqueued = h.queue.isChecked(); + boolean flag = !h.queue.isChecked(); + h.queue.setChecked(flag); + mission.setEnqueued(flag); updateProgress(h); return true; + case R.id.retry: + mission.psContinue(true); + return true; + case R.id.cancel: + mission.psContinue(false); + return false; } } switch (id) { - case R.id.open: - return viewWithFileProvider(h.item.mission.getDownloadedFile()); + case R.id.menu_item_share: + shareFile(h.item.mission); + return true; case R.id.delete: if (mDeleter == null) { mDownloadManager.deleteMission(h.item.mission); @@ -529,15 +604,42 @@ public class MissionAdapter extends Adapter { } public void setClearButton(MenuItem clearButton) { - if (mClear == null) clearButton.setVisible(mIterator.hasFinishedMissions()); + if (mClear == null) + clearButton.setVisible(mIterator.hasFinishedMissions()); + 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() { int flag = mIterator.getOldListSize() > 0 ? View.GONE : View.VISIBLE; if (mEmptyMessage.getVisibility() != flag) mEmptyMessage.setVisibility(flag); } + private void checkMasterButtonsVisibility() { + boolean[] state = mIterator.hasValidPendingMissions(); + + mStartButton.setVisible(state[0]); + mPauseButton.setVisible(state[1]); + } + + 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(Bundle bundle) { if (mDeleter != null) mDeleter.dispose(bundle); @@ -604,6 +706,8 @@ public class MissionAdapter extends Adapter { ProgressDrawable progress; PopupMenu popupMenu; + MenuItem retry; + MenuItem cancel; MenuItem start; MenuItem pause; MenuItem open; @@ -636,22 +740,34 @@ public class MissionAdapter extends Adapter { button.setOnClickListener(v -> showPopupMenu()); Menu menu = popupMenu.getMenu(); + retry = menu.findItem(R.id.retry); + cancel = menu.findItem(R.id.cancel); start = menu.findItem(R.id.start); 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); showError = menu.findItem(R.id.error_message_view); delete = menu.findItem(R.id.delete); source = menu.findItem(R.id.source); checksum = menu.findItem(R.id.checksum); - itemView.setOnClickListener((v) -> { + itemView.setHapticFeedbackEnabled(true); + + itemView.setOnClickListener(v -> { 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() { + retry.setVisible(false); + cancel.setVisible(false); start.setVisible(false); pause.setVisible(false); open.setVisible(false); @@ -664,7 +780,16 @@ public class MissionAdapter extends Adapter { DownloadMission mission = item.mission instanceof DownloadMission ? (DownloadMission) item.mission : null; if (mission != null) { - if (!mission.isPsRunning()) { + 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) { pause.setVisible(true); } else { diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index c4fd3b5fd..a3786a5e6 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -35,6 +35,8 @@ public class MissionsFragment extends Fragment { private boolean mLinear; private MenuItem mSwitch; private MenuItem mClear = null; + private MenuItem mStart = null; + private MenuItem mPause = null; private RecyclerView mList; private View mEmpty; @@ -54,9 +56,11 @@ public class MissionsFragment extends Fragment { mBinder = (DownloadManagerService.DMBinder) binder; mBinder.clearDownloadNotifications(); - mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mClear, mEmpty); + mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty); mAdapter.deleterLoad(mBundle, getView()); + setAdapterButtons(); + mBundle = null; mBinder.addMissionEventListener(mAdapter.getMessenger()); @@ -132,7 +136,7 @@ public class MissionsFragment extends Fragment { public void onAttach(Activity activity) { super.onAttach(activity); - mContext = activity.getApplicationContext(); + mContext = activity; } @@ -154,7 +158,11 @@ public class MissionsFragment extends Fragment { public void onPrepareOptionsMenu(Menu menu) { mSwitch = menu.findItem(R.id.switch_mode); 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); } @@ -168,6 +176,14 @@ public class MissionsFragment extends Fragment { case R.id.clear_list: mAdapter.clearFinishedDownloads(); return true; + case R.id.start_downloads: + item.setVisible(false); + mBinder.getDownloadManager().startAllMissions(); + return true; + case R.id.pause_downloads: + item.setVisible(false); + mBinder.getDownloadManager().pauseAllMissions(false); + mAdapter.ensurePausedMissions();// update items view default: return super.onOptionsItemSelected(item); } @@ -193,9 +209,9 @@ public class MissionsFragment extends Fragment { int icon; 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; + else + icon = isLight ? R.drawable.ic_list_black_24dp : R.drawable.ic_list_white_24dp; mSwitch.setIcon(icon); mSwitch.setTitle(mLinear ? R.string.grid : R.string.list); @@ -203,6 +219,13 @@ public class MissionsFragment extends Fragment { } } + private void setAdapterButtons() { + if (mClear == null || mStart == null || mPause == null) return; + + mAdapter.setClearButton(mClear); + mAdapter.setMasterButtons(mStart, mPause); + } + @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); diff --git a/app/src/main/res/drawable-hdpi/ic_pause_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_pause_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3770b9124c41ecdf986c9f15ebc9d33f4d308449 GIT binary patch literal 135 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbB{5)M8Ln2z=PBY|UFyLUW-FBh< z^_6eTubwCrvELNhVW`Bwps3y|k1|%Oc%$NbB0zF+ELn2z=UfamYV8Fq8u}3$h zU~LlHiQ}i74$0oo+;eGbN5?&*;_i(}_pkV>zv%dwbpNWax`*h|x)sb04GfG-EF1y~ m4)V>%_HNibuSV(DN^Z6)zQ%0|Gu8tQXYh3Ob6Mw<&;$UCzc1YY literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_pause_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_pause_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..6e81d3ad4f2029f7a521349688dff470b5858019 GIT binary patch literal 109 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjCY~;iAs(H{2@4 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_pause_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_pause_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..f5236e8aa89c24a1f42dbd298dbb08b871a05722 GIT binary patch literal 112 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`Gj=AJH&As(H{2@lo1`?|x>U_dm*KlFIf!UpaSA_<7IjIM7lCPgg&ebxsLQ02LECbpQYW literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..b94b2ae40da8a50607f4dbb92efc57ea7fac9e4d GIT binary patch literal 139 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUtAWs*^kc`H+Hx#)T6a-uy-`C&l zJ+?I>mRE`6^MMtAR&G=CoMfkETwVC+L0w4x9)^%y_A?7l12v+9F2?_Lx?Yo(bn_}W Vew)4B;Nvfl3Qt!*mvv4FO#oAVDPsTt literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_pause_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_pause_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..2691adeb30e1ada4ed84340315848782199c6ceb GIT binary patch literal 196 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!-JULvAr*7p-rC4}K!Jzh;G6yG z+#4K>9KRTe9-aF{>~#ER8=xwNKR22q+25~!*!Ox*v!!JMe|e1i0)+qp4Gs|&E+*E- jCI?6Ix&2Q-Hhllhz@PfGi~DL_K8Wk->gTe~DWM4fwX--o literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..15cb0b51ce27438dd5cfa97a2a8f84c456647ec5 GIT binary patch literal 206 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!Q$1ZALn`LHy=BOC*g%BU@&5mL zyBzdBT;9R6M)dI^1LiE%d)KV^fC?GvX3ji+EVBO2{<3=&Nso0mr&k}a{If~j?4DA9 wfCh&M3l|e>W0Qm90tK?Ui8U(hK#MAx_0^?TpQBfB_G~#&`Cq zW+yn_@)bVN@OZG@_uc&Gj6hXD&`?}$@cPg{(>)Kr-#d7xqQMr(VCmo#5LQrg@M)OA vV91!nbcDtNH=dk6QJVN)>K?>N3=HmeHaFkJH)U`C2NLmg^>bP0l+XkKR3S{3 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_pause_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_pause_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..12a49bc12b8a00c5acdbcac690d1a3cc266c466f GIT binary patch literal 254 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoM?GB}Ln`LHy>pQBh=Bl$<8%3? z#swVJ^6n2rP1r*hY(MfP{yHmA9SF#uJ+s;3d*ko-XQlakF3g(ymErw_`Eze5s5 + + + + + + + + + @@ -13,8 +22,8 @@ android:checkable="true"/> + android:id="@+id/menu_item_share" + android:title="@string/share" /> 系统拒绝该行动 下载失败 下载完成 - %已下载完毕 + %s已下载完毕 生成独特的名字 覆写 同名的已下载文件已经存在 diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index e75d905df..818efc74c 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -159,7 +159,7 @@ abrir en modo popup Si tienes ideas de; traducción, cambios de diseño, limpieza de código o cambios de código realmente fuertes—la ayuda siempre es bienvenida. Cuanto más se hace, mejor se pone! Leer licencia Contribuir - Suscribirse +Suscribirse Suscrito Canal no suscrito No se pudo cambiar la suscripción @@ -211,8 +211,8 @@ abrir en modo popup Vídeos Elemento eliminado - ¿Desea eliminar este elemento del historial de búsqueda? - Contenido de la página principal +¿Desea eliminar este elemento del historial de búsqueda? +Contenido de la página principal Página en blanco Página del kiosco Página de suscripción @@ -224,7 +224,7 @@ abrir en modo popup Kiosco Tendencias Top 50 - Mostrar sugerencia cuando se presiona el botón de segundo plano o popup en la página de detalles del vídeo +Mostrar sugerencia cuando se presiona el botón de segundo plano o popup en la página de detalles del vídeo En cola en el reproductor de fondo En cola en el reproductor popup Reproducir todo @@ -242,7 +242,7 @@ abrir en modo popup Comenzar a reproducir aquí Comenzar aquí en segundo plano Comenzar aquí en popup - Mostrar consejo \"Mantener para poner en la cola\" +Mostrar consejo \"Mantener para poner en la cola\" Nuevo y popular Mantener para poner en la cola Donar @@ -270,7 +270,7 @@ abrir en modo popup Reproductor de popup Obteniendo información… Cargando contenido solicitado - Importar base de datos +Importar base de datos Exportar base de datos Reemplazará su historial actual y sus suscripciones Exportar historial, suscripciones y listas de reproducción @@ -325,6 +325,7 @@ abrir en modo popup DIRECTO SINCRONIZAR Archivo + Archivo movido o eliminado No existe el directorio No existe la fuente del archivo/contenido El archivo no existe o insuficientes permisos para leerlo o escribir en él @@ -419,6 +420,8 @@ abrir en modo popup Sobrescribir Ya existe un archivo descargado con este nombre Hay una descarga en curso con este nombre + Hay una descarga pendiente con este nombre + Mostrar como grilla Mostrar como lista Limpiar descargas finalizadas @@ -426,8 +429,14 @@ abrir en modo popup Detener Intentos máximos Cantidad máxima de intentos antes de cancelar la descarga - Pausar al cambiar a datos moviles - Las descargas que no se pueden pausar serán reiniciadas + Interrumpir en redes medidas + Útil al cambiar a Datos Móviles, solo algunas descargas no se pueden suspender + Limitar cola de descarga + Solo se permitirá una descarga a la vez + Iniciar descargas + Pausar descargas + + Mostrar error Codigo @@ -439,9 +448,12 @@ abrir en modo popup No se puede conectar con el servidor El servidor no devolvio datos El servidor no acepta descargas multi-hilos, intente de nuevo con @string/msg_threads = 1 - El rango solicitado no se puede satisfacer + No se logro obtener el rango solicitado No encontrado Fallo el post-procesado + NewPipe se cerro mientras se trabajaba en el archivo + No hay suficiente espacio disponible en el dispositivo + Desuscribirse Nueva pestaña Elige la pestaña diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 865b68c24..37bc9eec6 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -25,6 +25,7 @@ + diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 3861f53d5..214a074c4 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -192,6 +192,7 @@ cross_network_downloads + downloads_queue_limit default_download_threads diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 154b8e0c4..afc6afeb3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -196,6 +196,7 @@ Invalid URL No video streams found No audio streams found + File moved or deleted No such folder No such file/content source The file doesn\'t exist or permission to read or write to it is lacking @@ -513,6 +514,8 @@ Overwrite A downloaded file with this name already exists There is a download in progress with this name + There is a pending download with this name + Show error Code @@ -527,13 +530,20 @@ Requested range not satisfiable Not found Post-processing failed + NewPipe was closed while working on the file + No space left on device + Clear finished downloads Continue your %s pending transfers from Downloads Stop Maximum retries Maximum number of attempts before canceling the download - Pause on switching to mobile data - Downloads that can not be paused will be restarted + Interrupt on metered networks + Useful when switching to mobile data, although some downloads cannot be suspended Close + Limit download queue + One download will run at the same time + Start downloads + Pause downloads \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index a7686dedc..51043718a 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -41,6 +41,7 @@ @drawable/ic_arrow_top_left_black_24dp @drawable/ic_more_vert_black_24dp @drawable/ic_play_arrow_black_24dp + @drawable/ic_pause_black_24dp @drawable/ic_settings_black_24dp @drawable/ic_whatshot_black_24dp @drawable/ic_channel_black_24dp @@ -119,6 +120,7 @@ @drawable/ic_list_white_24dp @drawable/ic_grid_white_24dp @drawable/ic_delete_white_24dp + @drawable/ic_pause_white_24dp @drawable/ic_settings_update_white @color/dark_separator_color diff --git a/app/src/main/res/xml/download_settings.xml b/app/src/main/res/xml/download_settings.xml index be015018a..9f32e7f2f 100644 --- a/app/src/main/res/xml/download_settings.xml +++ b/app/src/main/res/xml/download_settings.xml @@ -50,4 +50,11 @@ android:summary="@string/pause_downloads_on_mobile_desc" android:title="@string/pause_downloads_on_mobile" /> + + From f6b32823ba525c62241c65ba2327bc10a9cc107a Mon Sep 17 00:00:00 2001 From: kapodamy Date: Fri, 5 Apr 2019 14:45:39 -0300 Subject: [PATCH 13/30] Implement Storage Access Framework * re-work finished mission database * re-work DownloadMission and bump it Serializable version * keep the classic Java IO API * SAF Tree API support on Android Lollipop or higher * add wrapper for SAF stream opening * implement Closeable in SharpStream to replace the dispose() method * do required changes for this API: ** remove any file creation logic from DownloadInitializer ** make PostProcessing Serializable and reduce the number of iterations ** update all strings.xml files ** storage helpers: StoredDirectoryHelper & StoredFileHelper ** best effort to handle any kind of SAF errors/exceptions --- .../newpipe/download/DownloadActivity.java | 2 +- .../newpipe/download/DownloadDialog.java | 414 +++++++++++++----- .../newpipe/local/feed/FeedFragment.java | 4 +- .../player/playback/MediaSourceManager.java | 2 +- .../settings/DownloadSettingsFragment.java | 201 +++++++-- .../newpipe/settings/NewPipeSettings.java | 35 +- .../newpipe/streams/Mp4FromDashWriter.java | 2 +- .../schabi/newpipe/streams/WebMWriter.java | 2 +- .../newpipe/streams/io/SharpStream.java | 12 +- .../giga/get/DownloadInitializer.java | 36 +- .../us/shandian/giga/get/DownloadMission.java | 132 +++--- .../shandian/giga/get/DownloadRunnable.java | 11 +- .../giga/get/DownloadRunnableFallback.java | 16 +- .../us/shandian/giga/get/FinishedMission.java | 6 +- .../java/us/shandian/giga/get/Mission.java | 35 +- .../giga/get/sqlite/DownloadDataSource.java | 73 --- .../get/sqlite/DownloadMissionHelper.java | 112 ----- .../giga/get/sqlite/FinishedMissionStore.java | 223 ++++++++++ .../io/ChunkFileInputStream.java | 298 +++++++------ .../io/CircularFileWriter.java | 54 +-- .../java/us/shandian/giga/io/FileStream.java | 131 ++++++ .../us/shandian/giga/io/FileStreamSAF.java | 140 ++++++ .../io/SharpInputStream.java | 122 +++--- .../giga/io/StoredDirectoryHelper.java | 175 ++++++++ .../us/shandian/giga/io/StoredFileHelper.java | 301 +++++++++++++ .../giga/postprocessing/M4aNoDash.java | 6 +- .../giga/postprocessing/Mp4FromDashMuxer.java | 6 +- .../giga/postprocessing/Postprocessing.java | 108 +++-- .../giga/postprocessing/TtmlConverter.java | 9 +- .../giga/postprocessing/WebMMuxer.java | 6 +- .../giga/service/DownloadManager.java | 298 ++++++------- .../giga/service/DownloadManagerService.java | 204 ++++++--- .../shandian/giga/service/MissionState.java | 5 + .../giga/ui/adapter/MissionAdapter.java | 97 +++- .../us/shandian/giga/ui/common/Deleter.java | 52 +-- .../giga/ui/fragment/MissionsFragment.java | 57 ++- .../java/us/shandian/giga/util/Utility.java | 23 +- app/src/main/res/values-ar/strings.xml | 6 +- app/src/main/res/values-ca/strings.xml | 4 +- app/src/main/res/values-cmn/strings.xml | 6 +- app/src/main/res/values-da/strings.xml | 4 +- app/src/main/res/values-de/strings.xml | 6 +- app/src/main/res/values-es/strings.xml | 25 +- app/src/main/res/values-eu/strings.xml | 6 +- app/src/main/res/values-he/strings.xml | 6 +- app/src/main/res/values-id/strings.xml | 6 +- app/src/main/res/values-it/strings.xml | 6 +- app/src/main/res/values-ja/strings.xml | 12 +- app/src/main/res/values-ms/strings.xml | 6 +- app/src/main/res/values-nb-rNO/strings.xml | 6 +- app/src/main/res/values-nl-rBE/strings.xml | 6 +- app/src/main/res/values-nl/strings.xml | 6 +- app/src/main/res/values-pl/strings.xml | 6 +- app/src/main/res/values-pt-rBR/strings.xml | 6 +- app/src/main/res/values-pt/strings.xml | 6 +- app/src/main/res/values-ru/strings.xml | 6 +- app/src/main/res/values-tr/strings.xml | 6 +- app/src/main/res/values-vi/strings.xml | 6 +- app/src/main/res/values-zh-rTW/strings.xml | 6 +- app/src/main/res/values/settings_keys.xml | 17 +- app/src/main/res/values/strings.xml | 21 +- app/src/main/res/xml/download_settings.xml | 18 +- 62 files changed, 2439 insertions(+), 1180 deletions(-) delete mode 100644 app/src/main/java/us/shandian/giga/get/sqlite/DownloadDataSource.java delete mode 100644 app/src/main/java/us/shandian/giga/get/sqlite/DownloadMissionHelper.java create mode 100644 app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java rename app/src/main/java/us/shandian/giga/{postprocessing => }/io/ChunkFileInputStream.java (80%) rename app/src/main/java/us/shandian/giga/{postprocessing => }/io/CircularFileWriter.java (89%) create mode 100644 app/src/main/java/us/shandian/giga/io/FileStream.java create mode 100644 app/src/main/java/us/shandian/giga/io/FileStreamSAF.java rename app/src/main/java/us/shandian/giga/{postprocessing => }/io/SharpInputStream.java (91%) create mode 100644 app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java create mode 100644 app/src/main/java/us/shandian/giga/io/StoredFileHelper.java create mode 100644 app/src/main/java/us/shandian/giga/service/MissionState.java diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java index 7ee686a66..41971dfd4 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java @@ -55,7 +55,7 @@ public class DownloadActivity extends AppCompatActivity { private void updateFragments() { MissionsFragment fragment = new MissionsFragment(); - getFragmentManager().beginTransaction() + getSupportFragmentManager().beginTransaction() .replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG) .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .commit(); diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 0b4767133..4525c5988 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -1,8 +1,14 @@ package org.schabi.newpipe.download; +import android.app.Activity; +import android.content.ComponentName; import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; import android.content.SharedPreferences; +import android.net.Uri; import android.os.Bundle; +import android.os.IBinder; import android.preference.PreferenceManager; import android.support.annotation.IdRes; import android.support.annotation.NonNull; @@ -14,6 +20,7 @@ import android.support.v7.widget.Toolbar; import android.util.Log; import android.util.SparseArray; import android.view.LayoutInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; @@ -35,7 +42,8 @@ import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.VideoStream; 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.ListHelper; import org.schabi.newpipe.util.PermissionHelper; @@ -44,20 +52,27 @@ import org.schabi.newpipe.util.StreamItemAdapter; import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; import org.schabi.newpipe.util.ThemeHelper; +import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Locale; import icepick.Icepick; import icepick.State; 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.service.DownloadManager; import us.shandian.giga.service.DownloadManagerService; -import us.shandian.giga.service.DownloadManagerService.MissionCheck; +import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder; +import us.shandian.giga.service.MissionState; public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener { private static final String TAG = "DialogFragment"; private static final boolean DEBUG = MainActivity.DEBUG; + private static final int REQUEST_DOWNLOAD_PATH_SAF = 0x1230; @State protected StreamInfo currentInfo; @@ -82,7 +97,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck private EditText nameEditText; private Spinner streamsSpinner; - private RadioGroup radioVideoAudioGroup; + private RadioGroup radioStreamsGroup; private TextView threadsCountTextView; private SeekBar threadsSeekBar; @@ -162,7 +177,11 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck return; } - setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(getContext())); + final Context context = getContext(); + if (context == null) + throw new RuntimeException("Context was null"); + + setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context)); Icepick.restoreInstanceState(this, savedInstanceState); SparseArray> secondaryStreams = new SparseArray<>(4); @@ -179,9 +198,32 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck } } - this.videoStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedVideoStreams, secondaryStreams); - this.audioStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedAudioStreams); - this.subtitleStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedSubtitleStreams); + this.videoStreamsAdapter = new StreamItemAdapter<>(context, wrappedVideoStreams, secondaryStreams); + this.audioStreamsAdapter = new StreamItemAdapter<>(context, wrappedAudioStreams); + this.subtitleStreamsAdapter = new StreamItemAdapter<>(context, wrappedSubtitleStreams); + + Intent intent = new Intent(context, DownloadManagerService.class); + context.startService(intent); + + context.bindService(intent, new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName cname, IBinder service) { + DownloadManagerBinder mgr = (DownloadManagerBinder) service; + + mainStorageAudio = mgr.getMainStorageAudio(); + mainStorageVideo = mgr.getMainStorageVideo(); + downloadManager = mgr.getDownloadManager(); + + okButton.setEnabled(true); + + context.unbindService(this); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + // nothing to do + } + }, Context.BIND_AUTO_CREATE); } @Override @@ -206,8 +248,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck threadsCountTextView = view.findViewById(R.id.threads_count); threadsSeekBar = view.findViewById(R.id.threads); - radioVideoAudioGroup = view.findViewById(R.id.video_audio_group); - radioVideoAudioGroup.setOnCheckedChangeListener(this); + radioStreamsGroup = view.findViewById(R.id.video_audio_group); + radioStreamsGroup.setOnCheckedChangeListener(this); initToolbar(view.findViewById(R.id.toolbar)); setupDownloadOptions(); @@ -242,17 +284,17 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck disposables.clear(); disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams).subscribe(result -> { - if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.video_button) { + if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.video_button) { setupVideoSpinner(); } })); disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams).subscribe(result -> { - if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) { + if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) { setupAudioSpinner(); } })); disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams).subscribe(result -> { - if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) { + if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.subtitle_button) { setupSubtitleSpinner(); } })); @@ -270,17 +312,40 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck Icepick.saveInstanceState(this, outState); } + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == REQUEST_DOWNLOAD_PATH_SAF && resultCode == Activity.RESULT_OK) { + if (data.getData() == null) { + showFailedDialog(R.string.general_error); + return; + } + try { + continueSelectedDownload(new StoredFileHelper(getContext(), data.getData(), "")); + } catch (IOException e) { + showErrorActivity(e); + } + } + } + /*////////////////////////////////////////////////////////////////////////// // Inits //////////////////////////////////////////////////////////////////////////*/ private void initToolbar(Toolbar toolbar) { if (DEBUG) Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]"); + + boolean isLight = ThemeHelper.isLightThemeSelected(getActivity()); + okButton = toolbar.findViewById(R.id.okay); + okButton.setEnabled(false);// disable until the download service connection is done + toolbar.setTitle(R.string.download_dialog_title); - toolbar.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.setNavigationOnClickListener(v -> getDialog().dismiss()); + toolbar.setOnMenuItemClickListener(item -> { if (item.getItemId() == R.id.okay) { prepareSelectedDownload(); @@ -348,7 +413,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck public void onItemSelected(AdapterView parent, View view, int position, long id) { if (DEBUG) Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]"); - switch (radioVideoAudioGroup.getCheckedRadioButtonId()) { + switch (radioStreamsGroup.getCheckedRadioButtonId()) { case R.id.audio_button: selectedAudioIndex = position; break; @@ -372,9 +437,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck protected void setupDownloadOptions() { setRadioButtonsState(false); - final RadioButton audioButton = radioVideoAudioGroup.findViewById(R.id.audio_button); - final RadioButton videoButton = radioVideoAudioGroup.findViewById(R.id.video_button); - final RadioButton subtitleButton = radioVideoAudioGroup.findViewById(R.id.subtitle_button); + final RadioButton audioButton = radioStreamsGroup.findViewById(R.id.audio_button); + final RadioButton videoButton = radioStreamsGroup.findViewById(R.id.video_button); + final RadioButton subtitleButton = radioStreamsGroup.findViewById(R.id.subtitle_button); final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0; final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0; final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0; @@ -399,9 +464,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck } private void setRadioButtonsState(boolean enabled) { - radioVideoAudioGroup.findViewById(R.id.audio_button).setEnabled(enabled); - radioVideoAudioGroup.findViewById(R.id.video_button).setEnabled(enabled); - radioVideoAudioGroup.findViewById(R.id.subtitle_button).setEnabled(enabled); + radioStreamsGroup.findViewById(R.id.audio_button).setEnabled(enabled); + radioStreamsGroup.findViewById(R.id.video_button).setEnabled(enabled); + radioStreamsGroup.findViewById(R.id.subtitle_button).setEnabled(enabled); } private int getSubtitleIndexBy(List streams) { @@ -436,119 +501,248 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck return 0; } + StoredDirectoryHelper mainStorageAudio = null; + StoredDirectoryHelper mainStorageVideo = null; + DownloadManager downloadManager = null; + + MenuItem okButton = null; + + private String getNameEditText() { + return nameEditText.getText().toString().trim(); + } + + private void showFailedDialog(@StringRes int msg) { + new AlertDialog.Builder(getContext()) + .setMessage(msg) + .setNegativeButton(android.R.string.ok, null) + .create() + .show(); + } + + private void showErrorActivity(Exception e) { + ErrorActivity.reportError( + getContext(), + Collections.singletonList(e), + null, + null, + ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "-", "-", R.string.general_error) + ); + } + private void prepareSelectedDownload() { final Context context = getContext(); - Stream stream; - String location; - char kind; + StoredDirectoryHelper mainStorage; + MediaFormat format; + String mime; - String fileName = nameEditText.getText().toString().trim(); - if (fileName.isEmpty()) - fileName = FilenameUtils.createFilename(context, currentInfo.getName()); + // first, build the filename and get the output folder (if possible) - switch (radioVideoAudioGroup.getCheckedRadioButtonId()) { + String filename = getNameEditText() + "."; + if (filename.isEmpty()) { + filename = FilenameUtils.createFilename(context, currentInfo.getName()); + } + filename += "."; + + switch (radioStreamsGroup.getCheckedRadioButtonId()) { case R.id.audio_button: - stream = audioStreamsAdapter.getItem(selectedAudioIndex); - location = NewPipeSettings.getAudioDownloadPath(context); - kind = 'a'; + mainStorage = mainStorageAudio; + format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat(); + mime = format.mimeType; + filename += format.suffix; break; case R.id.video_button: - stream = videoStreamsAdapter.getItem(selectedVideoIndex); - location = NewPipeSettings.getVideoDownloadPath(context); - kind = 'v'; + mainStorage = mainStorageVideo; + format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat(); + mime = format.mimeType; + filename += format.suffix; break; case R.id.subtitle_button: - stream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex); - location = NewPipeSettings.getVideoDownloadPath(context);// assume that subtitle & video files go together - kind = 's'; + mainStorage = mainStorageVideo;// subtitle & video files go together + format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat(); + mime = format.mimeType; + filename += format == MediaFormat.TTML ? MediaFormat.SRT.suffix : format.suffix; + break; + default: + throw new RuntimeException("No stream selected"); + } + + if (mainStorage == null) { + // this part is called if... + // older android version running with SAF preferred + // save path not defined (via download settings) + + StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_PATH_SAF, filename, mime); + return; + } + + // check for existing file with the same name + Uri result = mainStorage.findFile(filename); + + if (result == null) { + // the file does not exists, create + StoredFileHelper storage = mainStorage.createFile(filename, mime); + if (storage == null || !storage.canWrite()) { + showFailedDialog(R.string.error_file_creation); + return; + } + + continueSelectedDownload(storage); + return; + } + + // the target filename is already use, try load + StoredFileHelper storage; + try { + storage = new StoredFileHelper(context, result, mime); + } catch (IOException e) { + showErrorActivity(e); + return; + } + + // check if is our file + MissionState state = downloadManager.checkForExistingMission(storage); + @StringRes int msgBtn; + @StringRes int 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: + msgBtn = R.string.overwrite; + msgBody = R.string.overwrite_unrelated_warning; break; default: return; } - int threads; + // handle user answer (overwrite or create another file with different name) + final String finalFilename = filename; + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.download_dialog_title) + .setMessage(msgBody) + .setPositiveButton(msgBtn, (dialog, which) -> { + dialog.dismiss(); - if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) { - threads = 1;// use unique thread for subtitles due small file size - fileName += ".srt";// final subtitle format - } else { - threads = threadsSeekBar.getProgress() + 1; - fileName += "." + stream.getFormat().getSuffix(); - } - - final String finalFileName = fileName; - - DownloadManagerService.checkForRunningMission(context, location, fileName, (MissionCheck result) -> { - @StringRes int msgBtn; - @StringRes int msgBody; - - switch (result) { - case Finished: - msgBtn = R.string.overwrite; - msgBody = R.string.overwrite_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; - default: - downloadSelected(context, stream, location, finalFileName, kind, threads); - return; - } - - // overwrite or unique name actions are done by the download manager - - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.download_dialog_title) - .setMessage(msgBody) - .setPositiveButton( - msgBtn, - (dialog, which) -> downloadSelected(context, stream, location, finalFileName, kind, threads) - ) - .setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel()) - .create() - .show(); - }); + StoredFileHelper storageNew; + switch (state) { + case Finished: + case Pending: + downloadManager.forgetMission(storage); + case None: + // try take (or steal) the file permissions + try { + storageNew = new StoredFileHelper(context, result, mainStorage.getTag()); + if (storageNew.canWrite()) + continueSelectedDownload(storageNew); + else + showFailedDialog(R.string.error_file_creation); + } catch (IOException e) { + showErrorActivity(e); + } + break; + case PendingRunning: + // FIXME: createUniqueFile() is not tested properly + storageNew = mainStorage.createUniqueFile(finalFilename, mime); + if (storageNew == null) + showFailedDialog(R.string.error_file_creation); + else + continueSelectedDownload(storageNew); + break; + } + }) + .setNegativeButton(android.R.string.cancel, null) + .create() + .show(); } - private void downloadSelected(Context context, Stream selectedStream, String location, String fileName, char kind, int threads) { + private void continueSelectedDownload(@NonNull StoredFileHelper storage) { + final Context context = getContext(); + + if (!storage.canWrite()) { + showFailedDialog(R.string.permission_denied); + return; + } + + // check if the selected file has to be overwritten, by simply checking its length + try { + if (storage.length() > 0) storage.truncate(); + } catch (IOException e) { + Log.e(TAG, "failed to overwrite the file: " + storage.getUri().toString(), e); + //showErrorActivity(e); + showFailedDialog(R.string.overwrite_failed); + return; + } + + Stream selectedStream; + char kind; + int threads = threadsSeekBar.getProgress() + 1; String[] urls; String psName = null; String[] psArgs = null; String secondaryStreamUrl = null; long nearLength = 0; - if (selectedStream instanceof AudioStream) { - if (selectedStream.getFormat() == MediaFormat.M4A) { - psName = Postprocessing.ALGORITHM_M4A_NO_DASH; - } - } else if (selectedStream instanceof VideoStream) { - SecondaryStreamHelper secondaryStream = videoStreamsAdapter - .getAllSecondary() - .get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream)); + // more download logic: select muxer, subtitle converter, etc. + switch (radioStreamsGroup.getCheckedRadioButtonId()) { + case R.id.audio_button: + threads = 1;// use unique thread for subtitles due small file size + kind = 'a'; + selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex); - if (secondaryStream != null) { - secondaryStreamUrl = secondaryStream.getStream().getUrl(); - psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_FROM_DASH_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; + if (selectedStream.getFormat() == MediaFormat.M4A) { + psName = Postprocessing.ALGORITHM_M4A_NO_DASH; } - } - } else if ((selectedStream instanceof SubtitlesStream) && selectedStream.getFormat() == MediaFormat.TTML) { - psName = Postprocessing.ALGORITHM_TTML_CONVERTER; - psArgs = new String[]{ - selectedStream.getFormat().getSuffix(), - "false",// ignore empty frames - "false",// detect youtube duplicate lines - }; + break; + case R.id.video_button: + kind = 'v'; + selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex); + + SecondaryStreamHelper secondaryStream = videoStreamsAdapter + .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 + 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) { @@ -557,8 +751,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck 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(); } } diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.java b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.java index f1bb01734..475627c08 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.java @@ -21,8 +21,8 @@ import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.fragments.list.BaseListFragment; -import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.local.subscription.SubscriptionService; +import org.schabi.newpipe.report.UserAction; import java.util.Collections; import java.util.HashSet; @@ -262,7 +262,7 @@ public class FeedFragment extends BaseListFragment, Voi * If chosen feed already displayed, then we request another feed from another * subscription, until the subscription table runs out of new items. *

- * 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 * 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. diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index db8cc797e..fb1a609cc 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -158,7 +158,7 @@ public class MediaSourceManager { * Dispose the manager and releases all message buses and loaders. * */ public void dispose() { - if (DEBUG) Log.d(TAG, "dispose() called."); + if (DEBUG) Log.d(TAG, "close() called."); debouncedSignal.onComplete(); debouncedLoader.dispose(); diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java index 82c6853d5..3737d1c17 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java @@ -2,26 +2,42 @@ package org.schabi.newpipe.settings; import android.app.Activity; import android.app.AlertDialog; +import android.content.Context; import android.content.Intent; +import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; +import android.support.annotation.StringRes; import android.support.v7.preference.Preference; import android.util.Log; - -import com.nononsenseapps.filepicker.Utils; +import android.widget.Toast; import org.schabi.newpipe.R; import org.schabi.newpipe.util.FilePickerActivityHelper; import java.io.File; +import java.io.IOException; +import java.net.URI; + +import us.shandian.giga.io.StoredDirectoryHelper; public class DownloadSettingsFragment extends BasePreferenceFragment { - private static final int REQUEST_DOWNLOAD_PATH = 0x1235; + private static final int REQUEST_DOWNLOAD_VIDEO_PATH = 0x1235; private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236; - private String DOWNLOAD_PATH_PREFERENCE; + private String DOWNLOAD_PATH_VIDEO_PREFERENCE; private String DOWNLOAD_PATH_AUDIO_PREFERENCE; + private String DOWNLOAD_STORAGE_API; + private String DOWNLOAD_STORAGE_API_DEFAULT; + + private Preference prefPathVideo; + private Preference prefPathAudio; + + private Context ctx; + @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -33,16 +49,100 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { addPreferencesFromResource(R.xml.download_settings); + + prefPathVideo = findPreference(DOWNLOAD_PATH_VIDEO_PREFERENCE); + prefPathAudio = findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE); + + updatePathPickers(usingJavaIO()); + + findPreference(DOWNLOAD_STORAGE_API).setOnPreferenceChangeListener((preference, value) -> { + boolean javaIO = DOWNLOAD_STORAGE_API_DEFAULT.equals(value); + + if (!javaIO && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Toast.makeText(ctx, R.string.download_pick_path, Toast.LENGTH_LONG).show(); + + // forget save paths + forgetSAFTree(DOWNLOAD_PATH_VIDEO_PREFERENCE); + forgetSAFTree(DOWNLOAD_PATH_AUDIO_PREFERENCE); + + defaultPreferences.edit() + .putString(DOWNLOAD_PATH_VIDEO_PREFERENCE, "") + .putString(DOWNLOAD_PATH_AUDIO_PREFERENCE, "") + .apply(); + + updatePreferencesSummary(); + } + + updatePathPickers(javaIO); + return true; + }); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + ctx = context; + } + + @Override + public void onDetach() { + super.onDetach(); + ctx = null; + findPreference(DOWNLOAD_STORAGE_API).setOnPreferenceChangeListener(null); } private void initKeys() { - DOWNLOAD_PATH_PREFERENCE = getString(R.string.download_path_key); + DOWNLOAD_PATH_VIDEO_PREFERENCE = getString(R.string.download_path_video_key); DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key); + DOWNLOAD_STORAGE_API = getString(R.string.downloads_storage_api); + DOWNLOAD_STORAGE_API_DEFAULT = getString(R.string.downloads_storage_api_default); } private void updatePreferencesSummary() { - findPreference(DOWNLOAD_PATH_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_PREFERENCE, getString(R.string.download_path_summary))); - findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, getString(R.string.download_path_audio_summary))); + prefPathVideo.setSummary( + defaultPreferences.getString(DOWNLOAD_PATH_VIDEO_PREFERENCE, getString(R.string.download_path_summary)) + ); + prefPathAudio.setSummary( + defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, getString(R.string.download_path_audio_summary)) + ); + } + + private void updatePathPickers(boolean useJavaIO) { + boolean enabled = useJavaIO || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; + prefPathVideo.setEnabled(enabled); + prefPathAudio.setEnabled(enabled); + } + + private boolean usingJavaIO() { + return DOWNLOAD_STORAGE_API_DEFAULT.equals( + defaultPreferences.getString(DOWNLOAD_STORAGE_API, DOWNLOAD_STORAGE_API_DEFAULT) + ); + } + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + private void forgetSAFTree(String prefKey) { + + String oldPath = defaultPreferences.getString(prefKey, ""); + + if (oldPath != null && !oldPath.isEmpty() && oldPath.charAt(0) != File.separatorChar) { + try { + StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(ctx, Uri.parse(oldPath), null); + if (!mainStorage.isDirect()) { + mainStorage.revokePermissions(); + Log.i(TAG, "revokePermissions() [uri=" + oldPath + "] ¡success!"); + } + } catch (IOException err) { + Log.e(TAG, "Error revoking Tree uri permissions [uri=" + oldPath + "]", err); + } + } + } + + private void showMessageDialog(@StringRes int title, @StringRes int message) { + AlertDialog.Builder msg = new AlertDialog.Builder(ctx); + msg.setTitle(title); + msg.setMessage(message); + msg.setPositiveButton(android.R.string.ok, null); + msg.show(); } @Override @@ -51,17 +151,31 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { Log.d(TAG, "onPreferenceTreeClick() called with: preference = [" + preference + "]"); } - if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE) - || preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) { - Intent i = new Intent(getActivity(), FilePickerActivityHelper.class) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) - .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); + String key = preference.getKey(); + + if (key.equals(DOWNLOAD_PATH_VIDEO_PREFERENCE) || key.equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) { + boolean safPick = !usingJavaIO(); + + int request = 0; + if (key.equals(DOWNLOAD_PATH_VIDEO_PREFERENCE)) { + request = REQUEST_DOWNLOAD_VIDEO_PATH; + } else if (key.equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) { + request = REQUEST_DOWNLOAD_AUDIO_PATH; } + + Intent i; + if (safPick && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + .putExtra("android.content.extra.SHOW_ADVANCED", true) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS); + } else { + i = new Intent(getActivity(), FilePickerActivityHelper.class) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) + .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_DIR); + } + + startActivityForResult(i, request); } return super.onPreferenceTreeClick(preference); @@ -71,25 +185,50 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); 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) - && 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(); + if (resultCode != Activity.RESULT_OK) return; - defaultPreferences.edit().putString(key, path).apply(); + String key; + if (requestCode == REQUEST_DOWNLOAD_VIDEO_PATH) + key = DOWNLOAD_PATH_VIDEO_PREFERENCE; + else if (requestCode == REQUEST_DOWNLOAD_AUDIO_PATH) + key = DOWNLOAD_PATH_AUDIO_PREFERENCE; + else + return; + + Uri uri = data.getData(); + if (uri == null) { + showMessageDialog(R.string.general_error, R.string.invalid_directory); + return; + } + + if (!usingJavaIO() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // steps: + // 1. acquire permissions on the new save path + // 2. save the new path, if step(1) was successful + + try { + StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(ctx, uri, null); + mainStorage.acquirePermissions(); + Log.i(TAG, "acquirePermissions() [uri=" + uri.toString() + "] ¡success!"); + } catch (IOException err) { + Log.e(TAG, "Error acquiring permissions on " + uri.toString()); + showMessageDialog(R.string.general_error, R.string.no_available_dir); + return; + } + + defaultPreferences.edit().putString(key, uri.toString()).apply(); + } else { + defaultPreferences.edit().putString(key, uri.toString()).apply(); updatePreferencesSummary(); - File target = new File(path); - if (!target.canWrite()) { - AlertDialog.Builder msg = new AlertDialog.Builder(getContext()); - msg.setTitle(R.string.download_to_sdcard_error_title); - msg.setMessage(R.string.download_to_sdcard_error_message); - msg.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { }); - msg.show(); - } + File target = new File(URI.create(uri.toString())); + if (!target.canWrite()) + showMessageDialog(R.string.download_to_sdcard_error_title, R.string.download_to_sdcard_error_message); } } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java index 2a0e2645b..f153cf23a 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java +++ b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java @@ -70,37 +70,23 @@ public class NewPipeSettings { getAudioDownloadFolder(context); } - public static File getVideoDownloadFolder(Context context) { - return getDir(context, R.string.download_path_key, Environment.DIRECTORY_MOVIES); + private static void getVideoDownloadFolder(Context context) { + getDir(context, R.string.download_path_video_key, Environment.DIRECTORY_MOVIES); } - public static String getVideoDownloadPath(Context context) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - final String key = context.getString(R.string.download_path_key); - return prefs.getString(key, Environment.DIRECTORY_MOVIES); + private static void getAudioDownloadFolder(Context context) { + getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC); } - public static File getAudioDownloadFolder(Context context) { - 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) { + private static void getDir(Context context, int keyID, String defaultDirectoryName) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); final String key = context.getString(keyID); 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(); - spEditor.putString(key, getNewPipeChildFolderPathForDir(dir)); + spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName))); spEditor.apply(); - return dir; } @NonNull @@ -110,8 +96,13 @@ public class NewPipeSettings { public static void resetDownloadFolders(Context context) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + prefs.edit() + .putString(context.getString(R.string.downloads_storage_api), context.getString(R.string.downloads_storage_api_default)) + .apply(); + resetDownloadFolder(prefs, context.getString(R.string.download_path_audio_key), Environment.DIRECTORY_MUSIC); - resetDownloadFolder(prefs, context.getString(R.string.download_path_key), Environment.DIRECTORY_MOVIES); + resetDownloadFolder(prefs, context.getString(R.string.download_path_video_key), Environment.DIRECTORY_MOVIES); } private static void resetDownloadFolder(SharedPreferences prefs, String key, String defaultDirectoryName) { diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java index 5a4efbe32..61f793e5d 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java @@ -120,7 +120,7 @@ public class Mp4FromDashWriter { parsed = true; for (SharpStream src : sourceTracks) { - src.dispose(); + src.close(); } tracks = null; diff --git a/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java index eba2bbb87..26b9cbebf 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java @@ -107,7 +107,7 @@ public class WebMWriter { parsed = true; for (SharpStream src : sourceTracks) { - src.dispose(); + src.close(); } sourceTracks = null; diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java b/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java index ea2f60837..5950ba3dd 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java +++ b/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java @@ -1,11 +1,12 @@ package org.schabi.newpipe.streams.io; +import java.io.Closeable; import java.io.IOException; /** * based on c# */ -public abstract class SharpStream { +public abstract class SharpStream implements Closeable { public abstract int read() throws IOException; @@ -19,9 +20,10 @@ public abstract class SharpStream { public abstract void rewind() throws IOException; - public abstract void dispose(); + public abstract boolean isClosed(); - public abstract boolean isDisposed(); + @Override + public abstract void close(); public abstract boolean canRewind(); @@ -54,4 +56,8 @@ public abstract class SharpStream { public void seek(long offset) throws IOException { throw new IOException("Not implemented"); } + + public long length() throws IOException { + throw new UnsupportedOperationException("Unsupported operation"); + } } diff --git a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java index abc934878..1e05983d8 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java @@ -3,10 +3,10 @@ package us.shandian.giga.get; import android.support.annotation.NonNull; 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.RandomAccessFile; import java.net.HttpURLConnection; import java.nio.channels.ClosedByInterruptException; @@ -111,34 +111,10 @@ public class DownloadInitializer extends Thread { if (!mMission.running || Thread.interrupted()) return; } - File file; - if (mMission.current == 0) { - file = new File(mMission.location); - if (!Utility.mkdir(file, true)) { - mMission.notifyError(DownloadMission.ERROR_PATH_CREATION, null); - return; - } - - file = new File(file, mMission.name); - - // if the name is used by another process, delete it - if (file.exists() && !file.isFile() && !file.delete()) { - mMission.notifyError(DownloadMission.ERROR_FILE_CREATION, null); - return; - } - - if (!file.exists() && !file.createNewFile()) { - mMission.notifyError(DownloadMission.ERROR_FILE_CREATION, null); - return; - } - } else { - file = new File(mMission.location, mMission.name); - } - - RandomAccessFile af = new RandomAccessFile(file, "rw"); - af.setLength(mMission.offsets[mMission.current] + mMission.length); - af.seek(mMission.offsets[mMission.current]); - af.close(); + SharpStream fs = mMission.storage.getStream(); + fs.setLength(mMission.offsets[mMission.current] + mMission.length); + fs.seek(mMission.offsets[mMission.current]); + fs.close(); if (!mMission.running || Thread.interrupted()) return; diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMission.java b/app/src/main/java/us/shandian/giga/get/DownloadMission.java index b8849482a..9ec3418b0 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -2,6 +2,7 @@ package us.shandian.giga.get; import android.os.Handler; import android.os.Message; +import android.support.annotation.NonNull; import android.util.Log; import java.io.File; @@ -17,6 +18,7 @@ import java.util.List; import javax.net.ssl.SSLException; +import us.shandian.giga.io.StoredFileHelper; import us.shandian.giga.postprocessing.Postprocessing; import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.util.Utility; @@ -24,7 +26,7 @@ import us.shandian.giga.util.Utility; import static org.schabi.newpipe.BuildConfig.DEBUG; 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; final static int BLOCK_SIZE = 512 * 1024; @@ -43,6 +45,7 @@ public class DownloadMission extends Mission { public static final int ERROR_POSTPROCESSING_STOPPED = 1008; public static final int ERROR_POSTPROCESSING_HOLD = 1009; public static final int ERROR_INSUFFICIENT_STORAGE = 1010; + public static final int ERROR_PROGRESS_LOST = 1011; public static final int ERROR_HTTP_NO_CONTENT = 204; public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206; @@ -71,16 +74,6 @@ public class DownloadMission extends Mission { */ 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: * 0: ready @@ -88,12 +81,12 @@ public class DownloadMission extends Mission { * 2: completed * 3: hold */ - public volatile int postprocessingState; + public volatile int psState; /** - * Indicate if the post-processing algorithm works on the same file + * the post-processing algorithm instance */ - public boolean postprocessingThis; + public transient Postprocessing psAlgorithm; /** * The current resource to download, see {@code urls[current]} and {@code offsets[current]} @@ -138,36 +131,23 @@ public class DownloadMission extends Mission { public transient volatile Thread[] threads = new Thread[0]; private transient Thread init = null; - protected DownloadMission() { } - public DownloadMission(String url, String name, String location, char kind) { - this(new String[]{url}, name, location, kind, null, null); - } - - public DownloadMission(String[] urls, String name, String location, char kind, String postprocessingName, String[] postprocessingArgs) { - if (name == null) throw new NullPointerException("name is null"); - if (name.isEmpty()) throw new IllegalArgumentException("name is empty"); + public DownloadMission(String[] urls, StoredFileHelper storage, char kind, Postprocessing psInstance) { if (urls == null) throw new NullPointerException("urls is null"); if (urls.length < 1) throw new IllegalArgumentException("urls is empty"); - if (location == null) throw new NullPointerException("location is null"); - if (location.isEmpty()) throw new IllegalArgumentException("location is empty"); this.urls = urls; - this.name = name; - this.location = location; this.kind = kind; this.offsets = new long[urls.length]; this.enqueued = true; this.maxRetry = 3; + this.storage = storage; - if (postprocessingName != null) { - Postprocessing algorithm = Postprocessing.getAlgorithm(postprocessingName, null); - this.postprocessingThis = algorithm.worksOnSameFile; - this.offsets[0] = algorithm.recommendedReserve; - this.postprocessingName = postprocessingName; - this.postprocessingArgs = postprocessingArgs; + if (psInstance != null) { + this.psAlgorithm = psInstance; + this.offsets[0] = psInstance.recommendedReserve; } else { if (DEBUG && urls.length > 1) { Log.w(TAG, "mission created with multiple urls ¿missing post-processing algorithm?"); @@ -359,22 +339,12 @@ public class DownloadMission extends Mission { Log.e(TAG, "notifyError() code = " + code, err); if (err instanceof IOException) { - if (err.getMessage().contains("Permission denied")) { + if (storage.canWrite() || err.getMessage().contains("Permission denied")) { code = ERROR_PERMISSION_DENIED; err = null; - } else if (err.getMessage().contains("write failed: ENOSPC")) { + } else if (err.getMessage().contains("ENOSPC")) { code = ERROR_INSUFFICIENT_STORAGE; err = null; - } else { - try { - File storage = new File(location); - if (storage.canWrite() && storage.getUsableSpace() < (getLength() - done)) { - code = ERROR_INSUFFICIENT_STORAGE; - err = null; - } - } catch (SecurityException e) { - // is a permission error - } } } @@ -433,11 +403,11 @@ public class DownloadMission extends Mission { action = "Failed"; } - Log.d(TAG, action + " postprocessing on " + location + File.separator + name); + Log.d(TAG, action + " postprocessing on " + storage.getName()); synchronized (blockState) { // don't return without fully write the current state - postprocessingState = state; + psState = state; Utility.writeToFile(metadata, DownloadMission.this); } } @@ -456,7 +426,7 @@ public class DownloadMission extends Mission { running = true; errCode = ERROR_NOTHING; - if (current >= urls.length && postprocessingName != null) { + if (current >= urls.length && psAlgorithm != null) { runAsync(1, () -> { if (doPostprocessing()) { running = false; @@ -593,7 +563,7 @@ public class DownloadMission extends Mission { * @return true, otherwise, false */ public boolean isFinished() { - return current >= urls.length && (postprocessingName == null || postprocessingState == 2); + return current >= urls.length && (psAlgorithm == null || psState == 2); } /** @@ -602,7 +572,13 @@ public class DownloadMission extends Mission { * @return {@code true} if this mission is unrecoverable */ public boolean isPsFailed() { - return postprocessingName != null && errCode == DownloadMission.ERROR_POSTPROCESSING && postprocessingThis; + switch (errCode) { + case ERROR_POSTPROCESSING: + case ERROR_POSTPROCESSING_STOPPED: + return psAlgorithm.worksOnSameFile; + } + + return false; } /** @@ -611,7 +587,7 @@ public class DownloadMission extends Mission { * @return true, otherwise, false */ public boolean isPsRunning() { - return postprocessingName != null && (postprocessingState == 1 || postprocessingState == 3); + return psAlgorithm != null && (psState == 1 || psState == 3); } /** @@ -625,7 +601,7 @@ public class DownloadMission extends Mission { public long getLength() { long calculated; - if (postprocessingState == 1 || postprocessingState == 3) { + if (psState == 1 || psState == 3) { calculated = length; } else { calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length; @@ -652,38 +628,60 @@ public class DownloadMission extends Mission { * @param recover {@code true} to retry, otherwise, {@code false} to cancel */ public void psContinue(boolean recover) { - postprocessingState = 1; + psState = 1; errCode = recover ? ERROR_NOTHING : ERROR_POSTPROCESSING; threads[0].interrupt(); } + /** + * changes the StoredFileHelper for another and saves the changes to the metadata file + * + * @param newStorage the new StoredFileHelper instance to use + */ + public void changeStorage(@NonNull StoredFileHelper newStorage) { + storage = newStorage; + // commit changes on the metadata file + runAsync(-2, this::writeThisToFile); + } + + /** + * Indicates whatever the backed storage is invalid + * + * @return {@code true}, if storage is invalid and cannot be used + */ + public boolean hasInvalidStorage() { + return errCode == ERROR_PROGRESS_LOST || storage == null || storage.isInvalid(); + } + + /** + * Indicates whatever is possible to start the mission + * + * @return {@code true} is this mission is "sane", otherwise, {@code false} + */ + public boolean canDownload() { + return !(isPsFailed() || errCode == ERROR_POSTPROCESSING_HOLD) && !isFinished() && !hasInvalidStorage(); + } + private boolean doPostprocessing() { - if (postprocessingName == null || postprocessingState == 2) return true; + if (psAlgorithm == null || psState == 2) return true; notifyPostProcessing(1); notifyProgress(0); if (DEBUG) - Thread.currentThread().setName("[" + TAG + "] post-processing = " + postprocessingName + " filename = " + name); + Thread.currentThread().setName("[" + TAG + "] ps = " + + psAlgorithm.getClass().getSimpleName() + + " filename = " + storage.getName() + ); threads = new Thread[]{Thread.currentThread()}; Exception exception = null; try { - Postprocessing - .getAlgorithm(postprocessingName, this) - .run(); + psAlgorithm.run(this); } catch (Exception err) { - StringBuilder args = new StringBuilder(" "); - if (postprocessingArgs != null) { - for (String arg : postprocessingArgs) { - args.append(", "); - args.append(arg); - } - args.delete(0, 1); - } - Log.e(TAG, String.format("Post-processing failed. algorithm = %s args = [%s]", postprocessingName, args), err); + Log.e(TAG, "Post-processing failed. " + psAlgorithm.toString(), err); if (errCode == ERROR_NOTHING) errCode = ERROR_POSTPROCESSING; @@ -733,7 +731,7 @@ public class DownloadMission extends Mission { // >=1: any download thread 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(); diff --git a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java index 244fbd47a..ced579b20 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java @@ -2,9 +2,10 @@ package us.shandian.giga.get; 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.RandomAccessFile; import java.net.HttpURLConnection; import java.nio.channels.ClosedByInterruptException; @@ -40,12 +41,12 @@ public class DownloadRunnable extends Thread { Log.d(TAG, mId + ":recovered: " + mMission.recovered); } - RandomAccessFile f; + SharpStream f; InputStream is = null; try { - f = new RandomAccessFile(mMission.getDownloadedFile(), "rw"); - } catch (FileNotFoundException e) { + f = mMission.storage.getStream(); + } catch (IOException e) { mMission.notifyError(e);// this never should happen return; } diff --git a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java index 4bcaeaf85..1a4b5d5b6 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java @@ -4,13 +4,13 @@ import android.annotation.SuppressLint; import android.support.annotation.NonNull; import android.util.Log; +import org.schabi.newpipe.streams.io.SharpStream; + import java.io.IOException; import java.io.InputStream; -import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.nio.channels.ClosedByInterruptException; - import us.shandian.giga.util.Utility; import static org.schabi.newpipe.BuildConfig.DEBUG; @@ -22,11 +22,10 @@ public class DownloadRunnableFallback extends Thread { private static final String TAG = "DownloadRunnableFallback"; private final DownloadMission mMission; - private final int mId = 1; private int mRetryCount = 0; private InputStream mIs; - private RandomAccessFile mF; + private SharpStream mF; private HttpURLConnection mConn; DownloadRunnableFallback(@NonNull DownloadMission mission) { @@ -43,11 +42,7 @@ public class DownloadRunnableFallback extends Thread { // nothing to do } - try { - if (mF != null) mF.close(); - } catch (IOException e) { - // ¿ejected media storage? ¿file deleted? ¿storage ran out of space? - } + if (mF != null) mF.close(); } @Override @@ -67,6 +62,7 @@ public class DownloadRunnableFallback extends Thread { try { long rangeStart = (mMission.unknownLength || start < 1) ? -1 : start; + int mId = 1; mConn = mMission.openConnection(mId, rangeStart, -1); mMission.establishConnection(mId, mConn); @@ -81,7 +77,7 @@ public class DownloadRunnableFallback extends Thread { if (!mMission.unknownLength) mMission.unknownLength = Utility.getContentLength(mConn) == -1; - mF = new RandomAccessFile(mMission.getDownloadedFile(), "rw"); + mF = mMission.storage.getStream(); mF.seek(mMission.offsets[mMission.current] + start); mIs = mConn.getInputStream(); diff --git a/app/src/main/java/us/shandian/giga/get/FinishedMission.java b/app/src/main/java/us/shandian/giga/get/FinishedMission.java index b7d6908a5..5540b44a1 100644 --- a/app/src/main/java/us/shandian/giga/get/FinishedMission.java +++ b/app/src/main/java/us/shandian/giga/get/FinishedMission.java @@ -1,16 +1,16 @@ package us.shandian.giga.get; +import android.support.annotation.NonNull; + public class FinishedMission extends Mission { public FinishedMission() { } - public FinishedMission(DownloadMission mission) { + public FinishedMission(@NonNull DownloadMission mission) { source = mission.source; length = mission.length;// ¿or mission.done? timestamp = mission.timestamp; - name = mission.name; - location = mission.location; kind = mission.kind; } } diff --git a/app/src/main/java/us/shandian/giga/get/Mission.java b/app/src/main/java/us/shandian/giga/get/Mission.java index 53c81b08b..ce201d960 100644 --- a/app/src/main/java/us/shandian/giga/get/Mission.java +++ b/app/src/main/java/us/shandian/giga/get/Mission.java @@ -1,12 +1,15 @@ package us.shandian.giga.get; -import java.io.File; +import android.net.Uri; +import android.support.annotation.NonNull; + import java.io.Serializable; -import java.text.SimpleDateFormat; import java.util.Calendar; +import us.shandian.giga.io.StoredFileHelper; + 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 @@ -23,28 +26,23 @@ public abstract class Mission implements Serializable { */ public long timestamp; - /** - * The filename - */ - public String name; - - /** - * The directory to store the download - */ - public String location; - /** * pre-defined content type */ public char kind; + /** + * The downloaded file + */ + public StoredFileHelper storage; + /** * get the target file on the storage * * @return File object */ - public File getDownloadedFile() { - return new File(location, name); + public Uri getDownloadedFileUri() { + return storage.getUri(); } /** @@ -53,8 +51,8 @@ public abstract class Mission implements Serializable { * @return {@code true] if and only if the file is successfully deleted, otherwise, {@code false} */ public boolean delete() { - deleted = true; - return getDownloadedFile().delete(); + if (storage != null) return storage.delete(); + return true; } /** @@ -62,10 +60,11 @@ public abstract class Mission implements Serializable { */ public transient boolean deleted = false; + @NonNull @Override public String toString() { Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(timestamp); - return "[" + calendar.getTime().toString() + "] " + location + File.separator + name; + return "[" + calendar.getTime().toString() + "] " + getDownloadedFileUri().getPath(); } } diff --git a/app/src/main/java/us/shandian/giga/get/sqlite/DownloadDataSource.java b/app/src/main/java/us/shandian/giga/get/sqlite/DownloadDataSource.java deleted file mode 100644 index 4b4d5d733..000000000 --- a/app/src/main/java/us/shandian/giga/get/sqlite/DownloadDataSource.java +++ /dev/null @@ -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 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 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); - } - } -} diff --git a/app/src/main/java/us/shandian/giga/get/sqlite/DownloadMissionHelper.java b/app/src/main/java/us/shandian/giga/get/sqlite/DownloadMissionHelper.java deleted file mode 100644 index 6dadc98c8..000000000 --- a/app/src/main/java/us/shandian/giga/get/sqlite/DownloadMissionHelper.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java b/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java new file mode 100644 index 000000000..6d63b9ff7 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java @@ -0,0 +1,223 @@ +package us.shandian.giga.get.sqlite; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.util.Log; + +import java.io.File; +import java.util.ArrayList; + +import us.shandian.giga.get.DownloadMission; +import us.shandian.giga.get.FinishedMission; +import us.shandian.giga.get.Mission; +import us.shandian.giga.io.StoredFileHelper; + +/** + * SQLite helper to store finished {@link us.shandian.giga.get.FinishedMission}'s + */ +public class FinishedMissionStore extends SQLiteOpenHelper { + + // TODO: use NewPipeSQLiteHelper ('s constants) when playlist branch is merged (?) + private static final String DATABASE_NAME = "downloads.db"; + + private static final int DATABASE_VERSION = 4; + + /** + * The table name of download missions (old) + */ + private static final String MISSIONS_TABLE_NAME_v2 = "download_missions"; + + /** + * The table name of download missions + */ + private static final String FINISHED_MISSIONS_TABLE_NAME = "finished_missions"; + + /** + * The key to the urls of a mission + */ + private static final String KEY_SOURCE = "url"; + + + /** + * The key to the done. + */ + private static final String KEY_DONE = "bytes_downloaded"; + + private static final String KEY_TIMESTAMP = "timestamp"; + + private static final String KEY_KIND = "kind"; + + private static final String KEY_PATH = "path"; + + /** + * The statement to create the table + */ + private static final String MISSIONS_CREATE_TABLE = + "CREATE TABLE " + FINISHED_MISSIONS_TABLE_NAME + " (" + + KEY_PATH + " TEXT NOT NULL, " + + KEY_SOURCE + " TEXT NOT NULL, " + + KEY_DONE + " INTEGER NOT NULL, " + + KEY_TIMESTAMP + " INTEGER NOT NULL, " + + KEY_KIND + " TEXT NOT NULL, " + + " UNIQUE(" + KEY_TIMESTAMP + ", " + KEY_PATH + "));"; + + + private Context context; + + public FinishedMissionStore(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + this.context = context; + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(MISSIONS_CREATE_TABLE); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion == 2) { + db.execSQL("ALTER TABLE " + MISSIONS_TABLE_NAME_v2 + " ADD COLUMN " + KEY_KIND + " TEXT;"); + oldVersion++; + } + + if (oldVersion == 3) { + final String KEY_LOCATION = "location"; + final String KEY_NAME = "name"; + + db.execSQL(MISSIONS_CREATE_TABLE); + + Cursor cursor = db.query(MISSIONS_TABLE_NAME_v2, null, null, + null, null, null, KEY_TIMESTAMP); + + int count = cursor.getCount(); + if (count > 0) { + db.beginTransaction(); + while (cursor.moveToNext()) { + ContentValues values = new ContentValues(); + values.put(KEY_SOURCE, cursor.getString(cursor.getColumnIndex(KEY_SOURCE))); + values.put(KEY_DONE, cursor.getString(cursor.getColumnIndex(KEY_DONE))); + values.put(KEY_TIMESTAMP, cursor.getLong(cursor.getColumnIndex(KEY_TIMESTAMP))); + values.put(KEY_KIND, cursor.getString(cursor.getColumnIndex(KEY_KIND))); + values.put(KEY_PATH, Uri.fromFile( + new File( + cursor.getString(cursor.getColumnIndex(KEY_LOCATION)), + cursor.getString(cursor.getColumnIndex(KEY_NAME)) + ) + ).toString()); + + db.insert(FINISHED_MISSIONS_TABLE_NAME, null, values); + } + db.setTransactionSuccessful(); + db.endTransaction(); + } + + cursor.close(); + db.execSQL("DROP TABLE " + MISSIONS_TABLE_NAME_v2); + } + } + + /** + * Returns all values of the download mission as ContentValues. + * + * @param downloadMission the download mission + * @return the content values + */ + private ContentValues getValuesOfMission(@NonNull Mission downloadMission) { + ContentValues values = new ContentValues(); + values.put(KEY_SOURCE, downloadMission.source); + values.put(KEY_PATH, downloadMission.storage.getUri().toString()); + values.put(KEY_DONE, downloadMission.length); + values.put(KEY_TIMESTAMP, downloadMission.timestamp); + values.put(KEY_KIND, String.valueOf(downloadMission.kind)); + return values; + } + + private FinishedMission getMissionFromCursor(Cursor cursor) { + if (cursor == null) throw new NullPointerException("cursor is null"); + + String kind = cursor.getString(cursor.getColumnIndex(KEY_KIND)); + if (kind == null || kind.isEmpty()) kind = "?"; + + String path = cursor.getString(cursor.getColumnIndexOrThrow(KEY_PATH)); + + FinishedMission mission = new FinishedMission(); + + mission.source = cursor.getString(cursor.getColumnIndexOrThrow(KEY_SOURCE)); + mission.length = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE)); + mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP)); + mission.kind = kind.charAt(0); + + try { + mission.storage = new StoredFileHelper(context, Uri.parse(path), ""); + } catch (Exception e) { + Log.e("FinishedMissionStore", "failed to load the storage path of: " + path, e); + mission.storage = new StoredFileHelper(path, "", ""); + } + + return mission; + } + + + ////////////////////////////////// + // Data source methods + /////////////////////////////////// + + public ArrayList loadFinishedMissions() { + SQLiteDatabase database = getReadableDatabase(); + Cursor cursor = database.query(FINISHED_MISSIONS_TABLE_NAME, null, null, + null, null, null, KEY_TIMESTAMP + " DESC"); + + int count = cursor.getCount(); + if (count == 0) return new ArrayList<>(1); + + ArrayList result = new ArrayList<>(count); + while (cursor.moveToNext()) { + result.add(getMissionFromCursor(cursor)); + } + + return result; + } + + public void addFinishedMission(DownloadMission downloadMission) { + if (downloadMission == null) throw new NullPointerException("downloadMission is null"); + SQLiteDatabase database = getWritableDatabase(); + ContentValues values = getValuesOfMission(downloadMission); + database.insert(FINISHED_MISSIONS_TABLE_NAME, null, values); + } + + public void deleteMission(Mission mission) { + if (mission == null) throw new NullPointerException("mission is null"); + String path = mission.getDownloadedFileUri().toString(); + + SQLiteDatabase database = getWritableDatabase(); + + if (mission instanceof FinishedMission) + database.delete(FINISHED_MISSIONS_TABLE_NAME, KEY_TIMESTAMP + " = ?, " + KEY_PATH + " = ?", new String[]{path}); + else + throw new UnsupportedOperationException("DownloadMission"); + } + + public void updateMission(Mission mission) { + if (mission == null) throw new NullPointerException("mission is null"); + SQLiteDatabase database = getWritableDatabase(); + ContentValues values = getValuesOfMission(mission); + String path = mission.getDownloadedFileUri().toString(); + + int rowsAffected; + + if (mission instanceof FinishedMission) + rowsAffected = database.update(FINISHED_MISSIONS_TABLE_NAME, values, KEY_PATH + " = ?", new String[]{path}); + else + throw new UnsupportedOperationException("DownloadMission"); + + if (rowsAffected != 1) { + Log.e("FinishedMissionStore", "Expected 1 row to be affected by update but got " + rowsAffected); + } + } +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/ChunkFileInputStream.java b/app/src/main/java/us/shandian/giga/io/ChunkFileInputStream.java similarity index 80% rename from app/src/main/java/us/shandian/giga/postprocessing/io/ChunkFileInputStream.java rename to app/src/main/java/us/shandian/giga/io/ChunkFileInputStream.java index ee2fcddd5..16a90fcee 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/io/ChunkFileInputStream.java +++ b/app/src/main/java/us/shandian/giga/io/ChunkFileInputStream.java @@ -1,150 +1,148 @@ -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; - -public class ChunkFileInputStream extends SharpStream { - - private RandomAccessFile source; - private final long offset; - private final long length; - private long position; - - public ChunkFileInputStream(File file, long start, long end, String mode) throws IOException { - source = new RandomAccessFile(file, mode); - offset = start; - length = end - start; - position = 0; - - if (length < 1) { - source.close(); - throw new IOException("The chunk is empty or invalid"); - } - if (source.length() < end) { - try { - throw new IOException(String.format("invalid file length. expected = %s found = %s", end, source.length())); - } finally { - source.close(); - } - } - - source.seek(offset); - } - - /** - * Get absolute position on file - * - * @return the position - */ - public long getFilePointer() { - return offset + position; - } - - @Override - public int read() throws IOException { - if ((position + 1) > length) { - return 0; - } - - int res = source.read(); - if (res >= 0) { - position++; - } - - return res; - } - - @Override - public int read(byte b[]) throws IOException { - return read(b, 0, b.length); - } - - @Override - public int read(byte b[], int off, int len) throws IOException { - if ((position + len) > length) { - len = (int) (length - position); - } - if (len == 0) { - return 0; - } - - int res = source.read(b, off, len); - position += res; - - return res; - } - - @Override - public long skip(long pos) throws IOException { - pos = Math.min(pos + position, length); - - if (pos == 0) { - return 0; - } - - source.seek(offset + pos); - - long oldPos = position; - position = pos; - - return pos - oldPos; - } - - @Override - public long available() { - return (int) (length - position); - } - - @SuppressWarnings("EmptyCatchBlock") - @Override - public void dispose() { - try { - source.close(); - } catch (IOException err) { - } finally { - source = null; - } - } - - @Override - public boolean isDisposed() { - return source == null; - } - - @Override - public void rewind() throws IOException { - position = 0; - source.seek(offset); - } - - @Override - public boolean canRewind() { - return true; - } - - @Override - public boolean canRead() { - return true; - } - - @Override - public boolean canWrite() { - return false; - } - - @Override - public void write(byte value) { - } - - @Override - public void write(byte[] buffer) { - } - - @Override - public void write(byte[] buffer, int offset, int count) { - } - -} +package us.shandian.giga.io; + +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.IOException; + +public class ChunkFileInputStream extends SharpStream { + + private SharpStream source; + private final long offset; + private final long length; + private long position; + + public ChunkFileInputStream(SharpStream target, long start) throws IOException { + this(target, start, target.length()); + } + + public ChunkFileInputStream(SharpStream target, long start, long end) throws IOException { + source = target; + offset = start; + length = end - start; + position = 0; + + if (length < 1) { + source.close(); + throw new IOException("The chunk is empty or invalid"); + } + if (source.length() < end) { + try { + throw new IOException(String.format("invalid file length. expected = %s found = %s", end, source.length())); + } finally { + source.close(); + } + } + + source.seek(offset); + } + + /** + * Get absolute position on file + * + * @return the position + */ + public long getFilePointer() { + return offset + position; + } + + @Override + public int read() throws IOException { + if ((position + 1) > length) { + return 0; + } + + int res = source.read(); + if (res >= 0) { + position++; + } + + return res; + } + + @Override + public int read(byte b[]) throws IOException { + return read(b, 0, b.length); + } + + @Override + public int read(byte b[], int off, int len) throws IOException { + if ((position + len) > length) { + len = (int) (length - position); + } + if (len == 0) { + return 0; + } + + int res = source.read(b, off, len); + position += res; + + return res; + } + + @Override + public long skip(long pos) throws IOException { + pos = Math.min(pos + position, length); + + if (pos == 0) { + return 0; + } + + source.seek(offset + pos); + + long oldPos = position; + position = pos; + + return pos - oldPos; + } + + @Override + public long available() { + return (int) (length - position); + } + + @SuppressWarnings("EmptyCatchBlock") + @Override + public void close() { + source.close(); + source = null; + } + + @Override + public boolean isClosed() { + return source == null; + } + + @Override + public void rewind() throws IOException { + position = 0; + source.seek(offset); + } + + @Override + public boolean canRewind() { + return true; + } + + @Override + public boolean canRead() { + return true; + } + + @Override + public boolean canWrite() { + return false; + } + + @Override + public void write(byte value) { + } + + @Override + public void write(byte[] buffer) { + } + + @Override + public void write(byte[] buffer, int offset, int count) { + } + +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFileWriter.java b/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java similarity index 89% rename from app/src/main/java/us/shandian/giga/postprocessing/io/CircularFileWriter.java rename to app/src/main/java/us/shandian/giga/io/CircularFileWriter.java index 4c4160fa3..650725a76 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFileWriter.java +++ b/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java @@ -1,4 +1,4 @@ -package us.shandian.giga.postprocessing.io; +package us.shandian.giga.io; import android.support.annotation.NonNull; @@ -7,7 +7,6 @@ import org.schabi.newpipe.streams.io.SharpStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; -import java.io.RandomAccessFile; public class CircularFileWriter extends SharpStream { @@ -26,7 +25,7 @@ public class CircularFileWriter extends SharpStream { private BufferedFile out; private BufferedFile aux; - public CircularFileWriter(File source, File temp, OffsetChecker checker) throws IOException { + public CircularFileWriter(SharpStream target, File temp, OffsetChecker checker) throws IOException { if (checker == null) { throw new NullPointerException("checker is null"); } @@ -38,7 +37,7 @@ public class CircularFileWriter extends SharpStream { } aux = new BufferedFile(temp); - out = new BufferedFile(source); + out = new BufferedFile(target); callback = checker; @@ -105,7 +104,7 @@ public class CircularFileWriter extends SharpStream { out.target.setLength(length); } - dispose(); + close(); return length; } @@ -114,13 +113,13 @@ public class CircularFileWriter extends SharpStream { * Close the file without flushing any buffer */ @Override - public void dispose() { + public void close() { if (out != null) { - out.dispose(); + out.close(); out = null; } if (aux != null) { - aux.dispose(); + aux.close(); aux = null; } } @@ -256,7 +255,7 @@ public class CircularFileWriter extends SharpStream { } @Override - public boolean isDisposed() { + public boolean isClosed() { return out == null; } @@ -339,30 +338,29 @@ public class CircularFileWriter extends SharpStream { class BufferedFile { - protected final RandomAccessFile target; + protected final SharpStream target; private long offset; protected long length; - private byte[] queue; + private byte[] queue = new byte[QUEUE_BUFFER_SIZE]; private int queueSize; BufferedFile(File file) throws FileNotFoundException { - queue = new byte[QUEUE_BUFFER_SIZE]; - target = new RandomAccessFile(file, "rw"); + this.target = new FileStream(file); + } + + BufferedFile(SharpStream target) { + this.target = target; } protected long getOffset() { return offset + queueSize;// absolute offset in the file } - protected void dispose() { - try { - queue = null; - target.close(); - } catch (IOException e) { - // nothing to do - } + protected void close() { + queue = null; + target.close(); } protected void write(byte b[], int off, int len) throws IOException { @@ -384,7 +382,7 @@ public class CircularFileWriter extends SharpStream { } } - protected void flush() throws IOException { + void flush() throws IOException { writeProof(queue, queueSize); offset += queueSize; queueSize = 0; @@ -404,7 +402,7 @@ public class CircularFileWriter extends SharpStream { return queue.length - queueSize; } - protected void reset() throws IOException { + void reset() throws IOException { offset = 0; length = 0; target.seek(0); @@ -415,7 +413,7 @@ public class CircularFileWriter extends SharpStream { target.seek(absoluteOffset); } - protected void writeProof(byte[] buffer, int length) throws IOException { + void writeProof(byte[] buffer, int length) throws IOException { if (onWriteError == null) { target.write(buffer, 0, length); return; @@ -436,14 +434,8 @@ public class CircularFileWriter extends SharpStream { @NonNull @Override public String toString() { - String absOffset; String absLength; - try { - absOffset = Long.toString(target.getFilePointer()); - } catch (IOException e) { - absOffset = "[" + e.getLocalizedMessage() + "]"; - } try { absLength = Long.toString(target.length()); } catch (IOException e) { @@ -451,8 +443,8 @@ public class CircularFileWriter extends SharpStream { } return String.format( - "offset=%s length=%s queue=%s absOffset=%s absLength=%s", - offset, length, queueSize, absOffset, absLength + "offset=%s length=%s queue=%s absLength=%s", + offset, length, queueSize, absLength ); } } diff --git a/app/src/main/java/us/shandian/giga/io/FileStream.java b/app/src/main/java/us/shandian/giga/io/FileStream.java new file mode 100644 index 000000000..5b2033324 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/io/FileStream.java @@ -0,0 +1,131 @@ +package us.shandian.giga.io; + +import android.support.annotation.NonNull; + +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; + +/** + * @author kapodamy + */ +public class FileStream extends SharpStream { + + public RandomAccessFile source; + + public FileStream(@NonNull File target) throws FileNotFoundException { + this.source = new RandomAccessFile(target, "rw"); + } + + public FileStream(@NonNull String path) throws FileNotFoundException { + this.source = new RandomAccessFile(path, "rw"); + } + + @Override + public int read() throws IOException { + return source.read(); + } + + @Override + public int read(byte b[]) throws IOException { + return source.read(b); + } + + @Override + public int read(byte b[], int off, int len) throws IOException { + return source.read(b, off, len); + } + + @Override + public long skip(long pos) throws IOException { + return source.skipBytes((int) pos); + } + + @Override + public long available() { + try { + return source.length() - source.getFilePointer(); + } catch (IOException e) { + return 0; + } + } + + @Override + public void close() { + if (source == null) return; + try { + source.close(); + } catch (IOException err) { + // nothing to do + } + source = null; + } + + @Override + public boolean isClosed() { + return source == null; + } + + @Override + public void rewind() throws IOException { + source.seek(0); + } + + @Override + public boolean canRewind() { + return true; + } + + @Override + public boolean canRead() { + return true; + } + + @Override + public boolean canWrite() { + return true; + } + + @Override + public boolean canSeek() { + return true; + } + + @Override + public boolean canSetLength() { + return true; + } + + @Override + public void write(byte value) throws IOException { + source.write(value); + } + + @Override + public void write(byte[] buffer) throws IOException { + source.write(buffer); + } + + @Override + public void write(byte[] buffer, int offset, int count) throws IOException { + source.write(buffer, offset, count); + } + + @Override + public void setLength(long length) throws IOException { + source.setLength(length); + } + + @Override + public void seek(long offset) throws IOException { + source.seek(offset); + } + + @Override + public long length() throws IOException { + return source.length(); + } +} diff --git a/app/src/main/java/us/shandian/giga/io/FileStreamSAF.java b/app/src/main/java/us/shandian/giga/io/FileStreamSAF.java new file mode 100644 index 000000000..cb4786280 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/io/FileStreamSAF.java @@ -0,0 +1,140 @@ +package us.shandian.giga.io; + +import android.content.ContentResolver; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.support.annotation.NonNull; +import android.util.Log; + +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.FileChannel; + +public class FileStreamSAF extends SharpStream { + + private final FileInputStream in; + private final FileOutputStream out; + private final FileChannel channel; + private final ParcelFileDescriptor file; + + private boolean disposed; + + public FileStreamSAF(@NonNull ContentResolver contentResolver, Uri fileUri) throws IOException { + // Notes: + // the file must exists first + // ¡read-write mode must allow seek! + // It is not guaranteed to work with files in the cloud (virtual files), tested in local storage devices + + file = contentResolver.openFileDescriptor(fileUri, "rw"); + + if (file == null) { + throw new IOException("Cannot get the ParcelFileDescriptor for " + fileUri.toString()); + } + + in = new FileInputStream(file.getFileDescriptor()); + out = new FileOutputStream(file.getFileDescriptor()); + channel = out.getChannel();// or use in.getChannel() + } + + @Override + public int read() throws IOException { + return in.read(); + } + + @Override + public int read(byte[] buffer) throws IOException { + return in.read(buffer); + } + + @Override + public int read(byte[] buffer, int offset, int count) throws IOException { + return in.read(buffer, offset, count); + } + + @Override + public long skip(long amount) throws IOException { + return in.skip(amount);// ¿or use channel.position(channel.position() + amount)? + } + + @Override + public long available() { + try { + return in.available(); + } catch (IOException e) { + return 0;// ¡but not -1! + } + } + + @Override + public void rewind() throws IOException { + seek(0); + } + + @Override + public void close() { + try { + disposed = true; + + file.close(); + in.close(); + out.close(); + channel.close(); + } catch (IOException e) { + Log.e("FileStreamSAF", "close() error", e); + } + } + + @Override + public boolean isClosed() { + return disposed; + } + + @Override + public boolean canRewind() { + return true; + } + + @Override + public boolean canRead() { + return true; + } + + @Override + public boolean canWrite() { + return true; + } + + public boolean canSetLength() { + return true; + } + + public boolean canSeek() { + return true; + } + + @Override + public void write(byte value) throws IOException { + out.write(value); + } + + @Override + public void write(byte[] buffer) throws IOException { + out.write(buffer); + } + + @Override + public void write(byte[] buffer, int offset, int count) throws IOException { + out.write(buffer, offset, count); + } + + public void setLength(long length) throws IOException { + channel.truncate(length); + } + + public void seek(long offset) throws IOException { + channel.position(offset); + } +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/SharpInputStream.java b/app/src/main/java/us/shandian/giga/io/SharpInputStream.java similarity index 91% rename from app/src/main/java/us/shandian/giga/postprocessing/io/SharpInputStream.java rename to app/src/main/java/us/shandian/giga/io/SharpInputStream.java index 586456d98..089101dfe 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/io/SharpInputStream.java +++ b/app/src/main/java/us/shandian/giga/io/SharpInputStream.java @@ -1,61 +1,61 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ -package us.shandian.giga.postprocessing.io; - -import android.support.annotation.NonNull; - -import org.schabi.newpipe.streams.io.SharpStream; - -import java.io.IOException; -import java.io.InputStream; - -/** - * Wrapper for the classic {@link java.io.InputStream} - * - * @author kapodamy - */ -public class SharpInputStream extends InputStream { - - private final SharpStream base; - - public SharpInputStream(SharpStream base) throws IOException { - if (!base.canRead()) { - throw new IOException("The provided stream is not readable"); - } - this.base = base; - } - - @Override - public int read() throws IOException { - return base.read(); - } - - @Override - public int read(@NonNull byte[] bytes) throws IOException { - return base.read(bytes); - } - - @Override - public int read(@NonNull byte[] bytes, int i, int i1) throws IOException { - return base.read(bytes, i, i1); - } - - @Override - public long skip(long l) throws IOException { - return base.skip(l); - } - - @Override - public int available() { - long res = base.available(); - return res > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) res; - } - - @Override - public void close() { - base.dispose(); - } -} +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package us.shandian.giga.io; + +import android.support.annotation.NonNull; + +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Wrapper for the classic {@link java.io.InputStream} + * + * @author kapodamy + */ +public class SharpInputStream extends InputStream { + + private final SharpStream base; + + public SharpInputStream(SharpStream base) throws IOException { + if (!base.canRead()) { + throw new IOException("The provided stream is not readable"); + } + this.base = base; + } + + @Override + public int read() throws IOException { + return base.read(); + } + + @Override + public int read(@NonNull byte[] bytes) throws IOException { + return base.read(bytes); + } + + @Override + public int read(@NonNull byte[] bytes, int i, int i1) throws IOException { + return base.read(bytes, i, i1); + } + + @Override + public long skip(long l) throws IOException { + return base.skip(l); + } + + @Override + public int available() { + long res = base.available(); + return res > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) res; + } + + @Override + public void close() { + base.close(); + } +} diff --git a/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java b/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java new file mode 100644 index 000000000..f5c2fd3f5 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java @@ -0,0 +1,175 @@ +package us.shandian.giga.io; + +import android.annotation.TargetApi; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; +import android.support.v4.provider.DocumentFile; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; + +public class StoredDirectoryHelper { + public final static int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; + + private File ioTree; + private DocumentFile docTree; + + private ContentResolver contentResolver; + + private String tag; + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public StoredDirectoryHelper(@NonNull Context context, @NonNull Uri path, String tag) throws IOException { + this.contentResolver = context.getContentResolver(); + this.tag = tag; + this.docTree = DocumentFile.fromTreeUri(context, path); + + if (this.docTree == null) + throw new IOException("Failed to create the tree from Uri"); + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + public StoredDirectoryHelper(@NonNull String location, String tag) { + ioTree = new File(location); + this.tag = tag; + } + + @Nullable + public StoredFileHelper createFile(String filename, String mime) { + StoredFileHelper storage; + + try { + if (docTree == null) { + storage = new StoredFileHelper(ioTree, filename, tag); + storage.sourceTree = Uri.fromFile(ioTree).toString(); + } else { + storage = new StoredFileHelper(docTree, contentResolver, filename, mime, tag); + storage.sourceTree = docTree.getUri().toString(); + } + } catch (IOException e) { + return null; + } + + storage.tag = tag; + + return storage; + } + + public StoredFileHelper createUniqueFile(String filename, String mime) { + ArrayList existingNames = new ArrayList<>(50); + + String ext; + + int dotIndex = filename.lastIndexOf('.'); + if (dotIndex < 0 || (dotIndex == filename.length() - 1)) { + ext = ""; + } else { + ext = filename.substring(dotIndex); + filename = filename.substring(0, dotIndex - 1); + } + + String name; + if (docTree == null) { + for (File file : ioTree.listFiles()) { + name = file.getName().toLowerCase(); + if (name.startsWith(filename)) existingNames.add(name); + } + } else { + for (DocumentFile file : docTree.listFiles()) { + name = file.getName(); + if (name == null) continue; + name = name.toLowerCase(); + if (name.startsWith(filename)) existingNames.add(name); + } + } + + boolean free = true; + String lwFilename = filename.toLowerCase(); + for (String testName : existingNames) { + if (testName.equals(lwFilename)) { + free = false; + break; + } + } + + if (free) return createFile(filename, mime); + + String[] sortedNames = existingNames.toArray(new String[0]); + Arrays.sort(sortedNames); + + String newName; + int downloadIndex = 0; + do { + newName = filename + " (" + downloadIndex + ")" + ext; + ++downloadIndex; + if (downloadIndex == 1000) { // Probably an error on our side + newName = System.currentTimeMillis() + ext; + break; + } + } while (Arrays.binarySearch(sortedNames, newName) >= 0); + + + return createFile(newName, mime); + } + + public boolean isDirect() { + return docTree == null; + } + + public Uri getUri() { + return docTree == null ? Uri.fromFile(ioTree) : docTree.getUri(); + } + + public boolean exists() { + return docTree == null ? ioTree.exists() : docTree.exists(); + } + + public String getTag() { + return tag; + } + + public void acquirePermissions() throws IOException { + if (docTree == null) return; + + try { + contentResolver.takePersistableUriPermission(docTree.getUri(), PERMISSION_FLAGS); + } catch (Throwable e) { + throw new IOException(e); + } + } + + public void revokePermissions() throws IOException { + if (docTree == null) return; + + try { + contentResolver.releasePersistableUriPermission(docTree.getUri(), PERMISSION_FLAGS); + } catch (Throwable e) { + throw new IOException(e); + } + } + + public Uri findFile(String filename) { + if (docTree == null) + return Uri.fromFile(new File(ioTree, filename)); + + // findFile() method is very slow + DocumentFile file = docTree.findFile(filename); + + return file == null ? null : file.getUri(); + } + + @NonNull + @Override + public String toString() { + return docTree == null ? Uri.fromFile(ioTree).toString() : docTree.getUri().toString(); + } + +} diff --git a/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java b/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java new file mode 100644 index 000000000..0db442f1c --- /dev/null +++ b/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java @@ -0,0 +1,301 @@ +package us.shandian.giga.io; + +import android.annotation.TargetApi; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.provider.DocumentsContract; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; +import android.support.v4.provider.DocumentFile; + +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.net.URI; + +public class StoredFileHelper implements Serializable { + private static final long serialVersionUID = 0L; + public static final String DEFAULT_MIME = "application/octet-stream"; + + private transient DocumentFile docFile; + private transient DocumentFile docTree; + private transient File ioFile; + private transient ContentResolver contentResolver; + + protected String source; + String sourceTree; + + protected String tag; + + private String srcName; + private String srcType; + + public StoredFileHelper(String filename, String mime, String tag) { + this.source = null;// this instance will be "invalid" see invalidate()/isInvalid() methods + + this.srcName = filename; + this.srcType = mime == null ? DEFAULT_MIME : mime; + + this.tag = tag; + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + StoredFileHelper(DocumentFile tree, ContentResolver contentResolver, String filename, String mime, String tag) throws IOException { + this.docTree = tree; + this.contentResolver = contentResolver; + + // this is very slow, because SAF does not allow overwrite + DocumentFile res = this.docTree.findFile(filename); + + if (res != null && res.exists() && res.isDirectory()) { + if (!res.delete()) + throw new IOException("Directory with the same name found but cannot delete"); + res = null; + } + + if (res == null) { + res = this.docTree.createFile(mime == null ? DEFAULT_MIME : mime, filename); + if (res == null) throw new IOException("Cannot create the file"); + } + + this.docFile = res; + this.source = res.getUri().toString(); + this.srcName = getName(); + this.srcType = getType(); + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + public StoredFileHelper(Context context, @NonNull Uri path, String tag) throws IOException { + this.source = path.toString(); + this.tag = tag; + + if (path.getScheme() == null || path.getScheme().equalsIgnoreCase("file")) { + this.ioFile = new File(URI.create(this.source)); + } else { + DocumentFile file = DocumentFile.fromSingleUri(context, path); + if (file == null) + throw new UnsupportedOperationException("Cannot get the file via SAF"); + + this.contentResolver = context.getContentResolver(); + this.docFile = file; + + try { + this.contentResolver.takePersistableUriPermission(docFile.getUri(), StoredDirectoryHelper.PERMISSION_FLAGS); + } catch (Exception e) { + throw new IOException(e); + } + } + + this.srcName = getName(); + this.srcType = getType(); + } + + public StoredFileHelper(File location, String filename, String tag) throws IOException { + this.ioFile = new File(location, filename); + this.tag = tag; + + if (this.ioFile.exists()) { + if (!this.ioFile.isFile() && !this.ioFile.delete()) + throw new IOException("The filename is already in use by non-file entity and cannot overwrite it"); + } else { + if (!this.ioFile.createNewFile()) + throw new IOException("Cannot create the file"); + } + + this.source = Uri.fromFile(this.ioFile).toString(); + this.srcName = getName(); + this.srcType = getType(); + } + + public static StoredFileHelper deserialize(@NonNull StoredFileHelper storage, Context context) throws IOException { + if (storage.isInvalid()) + return new StoredFileHelper(storage.srcName, storage.srcType, storage.tag); + + StoredFileHelper instance = new StoredFileHelper(context, Uri.parse(storage.source), storage.tag); + + if (storage.sourceTree != null) { + instance.docTree = DocumentFile.fromTreeUri(context, Uri.parse(instance.sourceTree)); + + if (instance.docTree == null) + throw new IOException("Cannot deserialize the tree, ¿revoked permissions?"); + } + + return instance; + } + + public static void requestSafWithFileCreation(@NonNull Fragment who, int requestCode, String filename, String mime) { + // SAF notes: + // ACTION_OPEN_DOCUMENT Do not let you create the file, useful for overwrite files + // ACTION_CREATE_DOCUMENT No overwrite support, useless the file provider resolve the conflict + + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType(mime) + .putExtra(Intent.EXTRA_TITLE, filename) + .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS) + .putExtra("android.content.extra.SHOW_ADVANCED", true);// hack, show all storage disks + + who.startActivityForResult(intent, requestCode); + } + + public SharpStream getStream() throws IOException { + invalid(); + + if (docFile == null) + return new FileStream(ioFile); + else + return new FileStreamSAF(contentResolver, docFile.getUri()); + } + + /** + * Indicates whatever if is possible access using the {@code java.io} API + * + * @return {@code true} for Java I/O API, otherwise, {@code false} for Storage Access Framework + */ + public boolean isDirect() { + invalid(); + + return docFile == null; + } + + public boolean isInvalid() { + return source == null; + } + + public Uri getUri() { + invalid(); + + return docFile == null ? Uri.fromFile(ioFile) : docFile.getUri(); + } + + public void truncate() throws IOException { + invalid(); + + try (SharpStream fs = getStream()) { + fs.setLength(0); + } + } + + public boolean delete() { + invalid(); + + if (docFile == null) return ioFile.delete(); + + boolean res = docFile.delete(); + + try { + int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; + contentResolver.releasePersistableUriPermission(docFile.getUri(), flags); + } catch (Exception ex) { + // ¿what happen? + } + + return res; + } + + public long length() { + invalid(); + + return docFile == null ? ioFile.length() : docFile.length(); + } + + public boolean canWrite() { + if (source == null) return false; + return docFile == null ? ioFile.canWrite() : docFile.canWrite(); + } + + public File getIOFile() { + return ioFile; + } + + public String getName() { + if (source == null) return srcName; + return docFile == null ? ioFile.getName() : docFile.getName(); + } + + public String getType() { + if (source == null) return srcType; + return docFile == null ? DEFAULT_MIME : docFile.getType();// not obligatory for Java IO + } + + public String getTag() { + return tag; + } + + public boolean existsAsFile() { + if (source == null) return false; + + boolean exists = docFile == null ? ioFile.exists() : docFile.exists(); + boolean asFile = docFile == null ? ioFile.isFile() : docFile.isFile();// ¿docFile.isVirtual() means is no-physical? + + return exists && asFile; + } + + public boolean create() { + invalid(); + + if (docFile == null) { + try { + return ioFile.createNewFile(); + } catch (IOException e) { + return false; + } + } + + if (docTree == null || docFile.getName() == null) return false; + + DocumentFile res = docTree.createFile(docFile.getName(), docFile.getType() == null ? DEFAULT_MIME : docFile.getType()); + if (res == null) return false; + + docFile = res; + return true; + } + + public void invalidate() { + if (source == null) return; + + srcName = getName(); + srcType = getType(); + + source = null; + + sourceTree = null; + docTree = null; + docFile = null; + ioFile = null; + contentResolver = null; + } + + private void invalid() { + if (source == null) + throw new IllegalStateException("In invalid state"); + } + + public boolean equals(StoredFileHelper storage) { + if (this.isInvalid() != storage.isInvalid()) return false; + if (this.isDirect() != storage.isDirect()) return false; + + if (this.isDirect()) + return this.ioFile.getPath().equalsIgnoreCase(storage.ioFile.getPath()); + + return DocumentsContract.getDocumentId( + this.docFile.getUri() + ).equalsIgnoreCase(DocumentsContract.getDocumentId( + storage.docFile.getUri() + )); + } + + @NonNull + @Override + public String toString() { + if (source == null) + return "[Invalid state] name=" + srcName + " type=" + srcType + " tag=" + tag; + else + return "sourceFile=" + source + " treeSource=" + (sourceTree == null ? "" : sourceTree) + " tag=" + tag; + } +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/M4aNoDash.java b/app/src/main/java/us/shandian/giga/postprocessing/M4aNoDash.java index fa0c2c7ae..ee7e4cba1 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/M4aNoDash.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/M4aNoDash.java @@ -6,12 +6,10 @@ import org.schabi.newpipe.streams.io.SharpStream; import java.io.IOException; -import us.shandian.giga.get.DownloadMission; - public class M4aNoDash extends Postprocessing { - M4aNoDash(DownloadMission mission) { - super(mission, 0, true); + M4aNoDash() { + super(0, true); } @Override diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java index 09f5d9661..98ab29dbb 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java @@ -5,15 +5,13 @@ import org.schabi.newpipe.streams.io.SharpStream; import java.io.IOException; -import us.shandian.giga.get.DownloadMission; - /** * @author kapodamy */ class Mp4FromDashMuxer extends Postprocessing { - Mp4FromDashMuxer(DownloadMission mission) { - super(mission, 2 * 1024 * 1024/* 2 MiB */, true); + Mp4FromDashMuxer() { + super(2 * 1024 * 1024/* 2 MiB */, true); } @Override diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java index df8549010..7bc32ea05 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java @@ -1,6 +1,7 @@ package us.shandian.giga.postprocessing; import android.os.Message; +import android.support.annotation.NonNull; import android.util.Log; import org.schabi.newpipe.streams.io.SharpStream; @@ -9,9 +10,9 @@ import java.io.File; import java.io.IOException; import us.shandian.giga.get.DownloadMission; -import us.shandian.giga.postprocessing.io.ChunkFileInputStream; -import us.shandian.giga.postprocessing.io.CircularFileWriter; -import us.shandian.giga.postprocessing.io.CircularFileWriter.OffsetChecker; +import us.shandian.giga.io.ChunkFileInputStream; +import us.shandian.giga.io.CircularFileWriter; +import us.shandian.giga.io.CircularFileWriter.OffsetChecker; import us.shandian.giga.service.DownloadManagerService; import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING; @@ -20,30 +21,41 @@ import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; public abstract class Postprocessing { - static final byte OK_RESULT = ERROR_NOTHING; + static transient final byte OK_RESULT = ERROR_NOTHING; - public static final String ALGORITHM_TTML_CONVERTER = "ttml"; - public static final String ALGORITHM_WEBM_MUXER = "webm"; - public static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4"; - public static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a"; + public transient static final String ALGORITHM_TTML_CONVERTER = "ttml"; + public transient static final String ALGORITHM_WEBM_MUXER = "webm"; + public transient static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4"; + public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a"; + + public static Postprocessing getAlgorithm(String algorithmName, String[] args) { + Postprocessing instance; - public static Postprocessing getAlgorithm(String algorithmName, DownloadMission mission) { if (null == algorithmName) { throw new NullPointerException("algorithmName"); } else switch (algorithmName) { case ALGORITHM_TTML_CONVERTER: - return new TtmlConverter(mission); + instance = new TtmlConverter(); + break; case ALGORITHM_WEBM_MUXER: - return new WebMMuxer(mission); + instance = new WebMMuxer(); + break; case ALGORITHM_MP4_FROM_DASH_MUXER: - return new Mp4FromDashMuxer(mission); + instance = new Mp4FromDashMuxer(); + break; case ALGORITHM_M4A_NO_DASH: - return new M4aNoDash(mission); + instance = new M4aNoDash(); + break; /*case "example-algorithm": - return new ExampleAlgorithm(mission);*/ + instance = new ExampleAlgorithm(mission);*/ default: throw new RuntimeException("Unimplemented post-processing algorithm: " + algorithmName); } + + instance.args = args; + instance.name = algorithmName; + + return instance; } /** @@ -61,32 +73,38 @@ public abstract class Postprocessing { /** * the download to post-process */ - protected DownloadMission mission; + protected transient DownloadMission mission; - Postprocessing(DownloadMission mission, int recommendedReserve, boolean worksOnSameFile) { - this.mission = mission; + public transient File cacheDir; + + private String[] args; + + private String name; + + Postprocessing(int recommendedReserve, boolean worksOnSameFile) { this.recommendedReserve = recommendedReserve; this.worksOnSameFile = worksOnSameFile; } - public void run() throws IOException { - File file = mission.getDownloadedFile(); + public void run(DownloadMission target) throws IOException { + this.mission = target; + File temp = null; CircularFileWriter out = null; int result; long finalLength = -1; mission.done = 0; - mission.length = file.length(); + mission.length = mission.storage.length(); if (worksOnSameFile) { ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length]; try { int i = 0; for (; i < sources.length - 1; i++) { - sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.offsets[i + 1], "rw"); + sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i], mission.offsets[i + 1]); } - sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.getDownloadedFile().length(), "rw"); + sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i]); if (test(sources)) { for (SharpStream source : sources) source.rewind(); @@ -97,7 +115,7 @@ public abstract class Postprocessing { * WARNING: never use rewind() in any chunk after any writing (especially on first chunks) * or the CircularFileWriter can lead to unexpected results */ - if (source.isDisposed() || source.available() < 1) { + if (source.isClosed() || source.available() < 1) { continue;// the selected source is not used anymore } @@ -107,18 +125,19 @@ public abstract class Postprocessing { return -1; }; - temp = new File(mission.location, mission.name + ".tmp"); + // TODO: use Context.getCache() for this operation + temp = new File(cacheDir, mission.storage.getName() + ".tmp"); - out = new CircularFileWriter(file, temp, checker); + out = new CircularFileWriter(mission.storage.getStream(), temp, checker); out.onProgress = this::progressReport; out.onWriteError = (err) -> { - mission.postprocessingState = 3; + mission.psState = 3; mission.notifyError(ERROR_POSTPROCESSING_HOLD, err); try { synchronized (this) { - while (mission.postprocessingState == 3) + while (mission.psState == 3) wait(); } } catch (InterruptedException e) { @@ -138,12 +157,12 @@ public abstract class Postprocessing { } } finally { for (SharpStream source : sources) { - if (source != null && !source.isDisposed()) { - source.dispose(); + if (source != null && !source.isClosed()) { + source.close(); } } if (out != null) { - out.dispose(); + out.close(); } if (temp != null) { //noinspection ResultOfMethodCallIgnored @@ -164,10 +183,9 @@ public abstract class Postprocessing { mission.errObject = new RuntimeException("post-processing algorithm returned " + result); } - if (result != OK_RESULT && worksOnSameFile) { - //noinspection ResultOfMethodCallIgnored - file.delete(); - } + if (result != OK_RESULT && worksOnSameFile) mission.storage.delete(); + + this.mission = null; } /** @@ -192,11 +210,11 @@ public abstract class Postprocessing { abstract int process(SharpStream out, SharpStream... sources) throws IOException; String getArgumentAt(int index, String defaultValue) { - if (mission.postprocessingArgs == null || index >= mission.postprocessingArgs.length) { + if (args == null || index >= args.length) { return defaultValue; } - return mission.postprocessingArgs[index]; + return args[index]; } private void progressReport(long done) { @@ -209,4 +227,22 @@ public abstract class Postprocessing { 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(); + } } diff --git a/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java b/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java index 390061840..bba0b299a 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java @@ -2,8 +2,8 @@ package us.shandian.giga.postprocessing; import android.util.Log; -import org.schabi.newpipe.streams.io.SharpStream; import org.schabi.newpipe.streams.SubtitleConverter; +import org.schabi.newpipe.streams.io.SharpStream; import org.xml.sax.SAXException; import java.io.IOException; @@ -12,18 +12,15 @@ import java.text.ParseException; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPathExpressionException; -import us.shandian.giga.get.DownloadMission; -import us.shandian.giga.postprocessing.io.SharpInputStream; - /** * @author kapodamy */ class TtmlConverter extends Postprocessing { private static final String TAG = "TtmlConverter"; - TtmlConverter(DownloadMission mission) { + TtmlConverter() { // due how XmlPullParser works, the xml is fully loaded on the ram - super(mission, 0, true); + super(0, true); } @Override diff --git a/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java index 2ffb0f08d..37295f2e3 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java @@ -7,15 +7,13 @@ import org.schabi.newpipe.streams.io.SharpStream; import java.io.IOException; -import us.shandian.giga.get.DownloadMission; - /** * @author kapodamy */ class WebMMuxer extends Postprocessing { - WebMMuxer(DownloadMission mission) { - super(mission, 2048 * 1024/* 2 MiB */, true); + WebMMuxer() { + super(2048 * 1024/* 2 MiB */, true); } @Override diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java index 58246beb1..3624fb6c2 100644 --- a/app/src/main/java/us/shandian/giga/service/DownloadManager.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java @@ -13,16 +13,15 @@ import org.schabi.newpipe.R; import java.io.File; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.FinishedMission; import us.shandian.giga.get.Mission; -import us.shandian.giga.get.sqlite.DownloadDataSource; -import us.shandian.giga.service.DownloadManagerService.DMChecker; -import us.shandian.giga.service.DownloadManagerService.MissionCheck; +import us.shandian.giga.get.sqlite.FinishedMissionStore; +import us.shandian.giga.io.StoredDirectoryHelper; +import us.shandian.giga.io.StoredFileHelper; import us.shandian.giga.util.Utility; import static org.schabi.newpipe.BuildConfig.DEBUG; @@ -36,7 +35,10 @@ public class DownloadManager { public final static int SPECIAL_PENDING = 1; 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 mMissionsPending = new ArrayList<>(); private final ArrayList mMissionsFinished; @@ -51,6 +53,9 @@ public class DownloadManager { boolean mPrefQueueLimit; private boolean mSelfMissionsControl; + StoredDirectoryHelper mMainStorageAudio; + StoredDirectoryHelper mMainStorageVideo; + /** * Create a new instance * @@ -62,7 +67,7 @@ public class DownloadManager { Log.d(TAG, "new DownloadManager instance. 0x" + Integer.toHexString(this.hashCode())); } - mDownloadDataSource = new DownloadDataSource(context); + mFinishedMissionStore = new FinishedMissionStore(context); mHandler = handler; mMissionsFinished = loadFinishedMissions(); mPendingMissionsDir = getPendingDir(context); @@ -71,7 +76,7 @@ public class DownloadManager { throw new RuntimeException("failed to create pending_downloads in data directory"); } - loadPendingMissions(); + loadPendingMissions(context); } private static File getPendingDir(@NonNull Context context) { @@ -92,29 +97,24 @@ public class DownloadManager { * Loads finished missions from the data source */ private ArrayList loadFinishedMissions() { - ArrayList finishedMissions = mDownloadDataSource.loadFinishedMissions(); + ArrayList finishedMissions = mFinishedMissionStore.loadFinishedMissions(); - // missions always is stored by creation order, simply reverse the list - ArrayList result = new ArrayList<>(finishedMissions.size()); + // check if the files exists, otherwise, forget the download for (int i = finishedMissions.size() - 1; i >= 0; i--) { FinishedMission mission = finishedMissions.get(i); - File file = mission.getDownloadedFile(); - if (!file.isFile()) { - if (DEBUG) { - Log.d(TAG, "downloaded file removed: " + file.getAbsolutePath()); - } - mDownloadDataSource.deleteMission(mission); - continue; + if (!mission.storage.existsAsFile()) { + if (DEBUG) Log.d(TAG, "downloaded file removed: " + mission.storage.getName()); + + mFinishedMissionStore.deleteMission(mission); + finishedMissions.remove(i); } - - result.add(mission); } - return result; + return finishedMissions; } - private void loadPendingMissions() { + private void loadPendingMissions(Context ctx) { File[] subs = mPendingMissionsDir.listFiles(); if (subs == null) { @@ -142,40 +142,63 @@ public class DownloadManager { continue; } - File dl = mis.getDownloadedFile(); - boolean exists = dl.exists(); + boolean exists; + try { + mis.storage = StoredFileHelper.deserialize(mis.storage, ctx); + exists = !mis.storage.isInvalid() && mis.storage.existsAsFile(); + + } catch (Exception ex) { + Log.e(TAG, "Failed to load the file source of " + mis.storage.toString()); + mis.storage.invalidate(); + exists = false; + } if (mis.isPsRunning()) { - if (mis.postprocessingThis) { + if (mis.psAlgorithm.worksOnSameFile) { // Incomplete post-processing results in a corrupted download file // because the selected algorithm works on the same file to save space. - if (exists && dl.isFile() && !dl.delete()) + if (exists && !mis.storage.delete()) Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath()); exists = true; } - mis.postprocessingState = 0; + mis.psState = 0; mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED; mis.errObject = null; - } 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()); + } else if (!exists) { + + StoredDirectoryHelper mainStorage = getMainStorage(mis.storage.getTag()); + + if (!mis.storage.isInvalid() && !mis.storage.create()) { + // using javaIO cannot recreate the file + // using SAF in older devices (no tree available) + // + // force the user to pick again the save path + mis.storage.invalidate(); + } else if (mainStorage != null) { + // if the user has changed the save path before this download, the original save path will be lost + StoredFileHelper newStorage = mainStorage.createFile(mis.storage.getName(), mis.storage.getType()); + if (newStorage == null) + mis.storage.invalidate(); + else + mis.storage = newStorage; + } + + if (mis.isInitialized()) { + // the progress is lost, reset mission state + DownloadMission m = new DownloadMission(mis.urls, mis.storage, mis.kind, mis.psAlgorithm); + m.timestamp = mis.timestamp; + m.threadCount = mis.threadCount; + m.source = mis.source; + m.nearLength = mis.nearLength; + m.enqueued = mis.enqueued; + m.errCode = DownloadMission.ERROR_PROGRESS_LOST; + mis = m; } - continue; } - if (!exists && mis.isInitialized()) { - // 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.nearLength = mis.nearLength; - m.setEnqueued(mis.enqueued); - mis = m; - } + if (mis.psAlgorithm != null) mis.psAlgorithm.cacheDir = ctx.getCacheDir(); mis.running = false; mis.recovered = exists; @@ -196,51 +219,15 @@ public class DownloadManager { /** * Start a new download mission * - * @param urls the list of urls to download - * @param location the location - * @param name the name of the file to create - * @param kind type of file (a: audio v: video s: subtitle ?: file-extension defined) - * @param threads the number of threads maximal used to download chunks of the file. - * @param 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 mission the new download mission to add and run (if possible) */ - void startMission(String[] urls, String location, String name, char kind, int threads, - String source, String psName, String[] psArgs, long nearLength) { + void startMission(DownloadMission mission) { synchronized (this) { - // check for existing pending download - DownloadMission pendingMission = getPendingMission(location, name); - - if (pendingMission != null) { - if (pendingMission.running) { - // generate unique filename (?) - try { - name = generateUniqueName(location, name); - } catch (Exception e) { - Log.e(TAG, "Unable to generate unique name", e); - name = System.currentTimeMillis() + name; - Log.i(TAG, "Using " + name); - } - } else { - // dispose the mission - mMissionsPending.remove(pendingMission); - mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED); - pendingMission.delete(); - } - } else { - // check for existing finished download and dispose (if exists) - int index = getFinishedMissionIndex(location, name); - if (index >= 0) mDownloadDataSource.deleteMission(mMissionsFinished.remove(index)); - } - - DownloadMission mission = new DownloadMission(urls, name, location, kind, psName, psArgs); mission.timestamp = System.currentTimeMillis(); - mission.threadCount = threads; - mission.source = source; mission.mHandler = mHandler; mission.maxRetry = mPrefMaxRetry; - mission.nearLength = nearLength; + // create metadata file while (true) { mission.metadata = new File(mPendingMissionsDir, String.valueOf(mission.timestamp)); if (!mission.metadata.isFile() && !mission.metadata.exists()) { @@ -261,6 +248,14 @@ public class DownloadManager { // Before continue, save the metadata in case the internet connection is not available Utility.writeToFile(mission.metadata, mission); + if (mission.storage == null) { + // noting to do here + mission.errCode = DownloadMission.ERROR_FILE_CREATION; + if (mission.errObject != null) + mission.errObject = new IOException("DownloadMission.storage == NULL"); + return; + } + boolean start = !mPrefQueueLimit || getRunningMissionsCount() < 1; if (canDownloadInCurrentNetwork() && start) { @@ -292,7 +287,7 @@ public class DownloadManager { mMissionsPending.remove(mission); } else if (mission instanceof FinishedMission) { mMissionsFinished.remove(mission); - mDownloadDataSource.deleteMission(mission); + mFinishedMissionStore.deleteMission(mission); } mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED); @@ -300,18 +295,35 @@ public class DownloadManager { } } + public void forgetMission(StoredFileHelper storage) { + synchronized (this) { + Mission mission = getAnyMission(storage); + if (mission == null) return; + + if (mission instanceof DownloadMission) { + mMissionsPending.remove(mission); + } else if (mission instanceof FinishedMission) { + mMissionsFinished.remove(mission); + mFinishedMissionStore.deleteMission(mission); + } + + mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED); + mission.storage = null; + mission.delete(); + } + } + /** - * Get a pending mission by its location and name + * Get a pending mission by its path * - * @param location the location - * @param name the name + * @param storage where the file possible is stored * @return the mission or null if no such mission exists */ @Nullable - private DownloadMission getPendingMission(String location, String name) { + private DownloadMission getPendingMission(StoredFileHelper storage) { for (DownloadMission mission : mMissionsPending) { - if (location.equalsIgnoreCase(mission.location) && name.equalsIgnoreCase(mission.name)) { + if (mission.storage.equals(storage)) { return mission; } } @@ -319,16 +331,14 @@ public class DownloadManager { } /** - * Get a finished mission by its location and name + * Get a finished mission by its path * - * @param location the location - * @param name the name + * @param storage where the file possible is stored * @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++) { - FinishedMission mission = mMissionsFinished.get(i); - if (location.equalsIgnoreCase(mission.location) && name.equalsIgnoreCase(mission.name)) { + if (mMissionsFinished.get(i).storage.equals(storage)) { return i; } } @@ -336,12 +346,12 @@ public class DownloadManager { return -1; } - public Mission getAnyMission(String location, String name) { + private Mission getAnyMission(StoredFileHelper storage) { synchronized (this) { - Mission mission = getPendingMission(location, name); + Mission mission = getPendingMission(storage); if (mission != null) return mission; - int idx = getFinishedMissionIndex(location, name); + int idx = getFinishedMissionIndex(storage); if (idx >= 0) return mMissionsFinished.get(idx); } @@ -382,7 +392,7 @@ public class DownloadManager { synchronized (this) { for (DownloadMission mission : mMissionsPending) { - if (mission.running || mission.isPsFailed() || mission.isFinished()) continue; + if (mission.running || !mission.canDownload()) continue; flag = true; mission.start(); @@ -392,58 +402,6 @@ public class DownloadManager { if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS); } - - /** - * Splits the filename into name and extension - *

- * 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. - *

- * e.g. "myName (1).txt" if the name "myName.txt" exists. - * - * @param location the location (to check for existing files) - * @param name the name of the file - * @return the unique file name - * @throws IllegalArgumentException if the location is not a directory - * @throws SecurityException if the location is not readable - */ - private static String generateUniqueName(String location, String name) { - if (location == null) throw new NullPointerException("location is null"); - if (name == null) throw new NullPointerException("name is null"); - File destination = new File(location); - if (!destination.isDirectory()) { - throw new IllegalArgumentException("location is not a directory: " + location); - } - final String[] nameParts = splitName(name); - String[] existingName = destination.list((dir, name1) -> name1.startsWith(nameParts[0])); - Arrays.sort(existingName); - String newName; - int downloadIndex = 0; - do { - newName = nameParts[0] + " (" + downloadIndex + ")." + nameParts[1]; - ++downloadIndex; - if (downloadIndex == 1000) { // Probably an error on our side - throw new RuntimeException("Too many existing files"); - } - } while (Arrays.binarySearch(existingName, newName) >= 0); - return newName; - } - /** * Set a pending download as finished * @@ -453,7 +411,7 @@ public class DownloadManager { synchronized (this) { mMissionsPending.remove(mission); mMissionsFinished.add(0, new FinishedMission(mission)); - mDownloadDataSource.addMission(mission); + mFinishedMissionStore.addFinishedMission(mission); } } @@ -474,7 +432,8 @@ public class DownloadManager { boolean flag = false; for (DownloadMission mission : mMissionsPending) { - if (mission.running || !mission.enqueued || mission.isFinished()) continue; + if (mission.running || !mission.enqueued || mission.isFinished() || mission.hasInvalidStorage()) + continue; resumeMission(mission); if (mPrefQueueLimit) return true; @@ -496,7 +455,7 @@ public class DownloadManager { public void forgetFinishedDownloads() { synchronized (this) { for (FinishedMission mission : mMissionsFinished) { - mDownloadDataSource.deleteMission(mission); + mFinishedMissionStore.deleteMission(mission); } mMissionsFinished.clear(); } @@ -523,7 +482,7 @@ public class DownloadManager { int paused = 0; synchronized (this) { for (DownloadMission mission : mMissionsPending) { - if (mission.isFinished() || mission.isPsRunning()) continue; + if (!mission.canDownload() || mission.isPsRunning()) continue; if (mission.running && isMetered) { paused++; @@ -565,24 +524,32 @@ public class DownloadManager { ), Toast.LENGTH_LONG).show(); } - void checkForRunningMission(String location, String name, DMChecker check) { - MissionCheck result = MissionCheck.None; - + public MissionState checkForExistingMission(StoredFileHelper storage) { synchronized (this) { - DownloadMission pending = getPendingMission(location, name); + DownloadMission pending = getPendingMission(storage); if (pending == null) { - if (getFinishedMissionIndex(location, name) >= 0) result = MissionCheck.Finished; + if (getFinishedMissionIndex(storage) >= 0) return MissionState.Finished; } else { if (pending.isFinished()) { - result = MissionCheck.Finished;// this never should happen (race-condition) + return MissionState.Finished;// this never should happen (race-condition) } else { - result = pending.running ? MissionCheck.PendingRunning : MissionCheck.Pending; + return pending.running ? MissionState.PendingRunning : MissionState.Pending; } } } - check.callback(result); + return MissionState.None; + } + + @Nullable + private StoredDirectoryHelper getMainStorage(@NonNull String tag) { + if (tag.equals(TAG_AUDIO)) return mMainStorageAudio; + if (tag.equals(TAG_VIDEO)) return mMainStorageVideo; + + Log.w(TAG, "Unknown download category, not [audio video]: " + String.valueOf(tag)); + + return null;// this never should happen } public class MissionIterator extends DiffUtil.Callback { @@ -689,7 +656,7 @@ public class DownloadManager { synchronized (DownloadManager.this) { for (DownloadMission mission : mMissionsPending) { - if (hidden.contains(mission) || mission.isPsFailed() || mission.isFinished()) + if (hidden.contains(mission) || mission.canDownload()) continue; if (mission.running) @@ -720,7 +687,14 @@ public class DownloadManager { @Override 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; } } diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index be1e20dd6..deed9e8e3 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -6,11 +6,9 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.ServiceConnection; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -21,12 +19,14 @@ import android.net.NetworkRequest; import android.net.Uri; import android.os.Binder; import android.os.Build; +import android.os.Environment; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.preference.PreferenceManager; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat.Builder; import android.support.v4.content.PermissionChecker; @@ -39,9 +39,13 @@ import org.schabi.newpipe.download.DownloadActivity; import org.schabi.newpipe.player.helper.LockManager; import java.io.File; +import java.io.IOException; import java.util.ArrayList; 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 static org.schabi.newpipe.BuildConfig.APPLICATION_ID; @@ -61,19 +65,19 @@ public class DownloadManagerService extends Service { private static final int DOWNLOADS_NOTIFICATION_ID = 1001; private static final 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_PATH = "DownloadManagerService.extra.path"; private static final String EXTRA_KIND = "DownloadManagerService.extra.kind"; private static final String EXTRA_THREADS = "DownloadManagerService.extra.threads"; private static final String EXTRA_POSTPROCESSING_NAME = "DownloadManagerService.extra.postprocessingName"; private static final String EXTRA_POSTPROCESSING_ARGS = "DownloadManagerService.extra.postprocessingArgs"; private static final String EXTRA_SOURCE = "DownloadManagerService.extra.source"; private static final String EXTRA_NEAR_LENGTH = "DownloadManagerService.extra.nearLength"; + private static final String EXTRA_MAIN_STORAGE_TAG = "DownloadManagerService.extra.tag"; private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished"; private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished"; - private DMBinder mBinder; + private DownloadManagerBinder mBinder; private DownloadManager mManager; private Notification mNotification; private Handler mHandler; @@ -110,10 +114,10 @@ public class DownloadManagerService extends Service { /** * notify media scanner on downloaded media file ... * - * @param file the downloaded file + * @param file the downloaded file uri */ - private void notifyMediaScanner(File file) { - sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file))); + private void notifyMediaScanner(Uri file) { + sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, file)); } @Override @@ -124,7 +128,7 @@ public class DownloadManagerService extends Service { Log.d(TAG, "onCreate"); } - mBinder = new DMBinder(); + mBinder = new DownloadManagerBinder(); mHandler = new Handler(Looper.myLooper()) { @Override public void handleMessage(Message msg) { @@ -186,10 +190,12 @@ public class DownloadManagerService extends Service { handlePreferenceChange(mPrefs, getString(R.string.downloads_queue_limit)); mLock = new LockManager(this); + + setupStorageAPI(true); } @Override - public int onStartCommand(Intent intent, int flags, int startId) { + public int onStartCommand(final Intent intent, int flags, int startId) { if (DEBUG) { Log.d(TAG, intent == null ? "Restarting" : "Starting"); } @@ -200,20 +206,7 @@ public class DownloadManagerService extends Service { String action = intent.getAction(); if (action != null) { if (action.equals(Intent.ACTION_RUN)) { - String[] urls = intent.getStringArrayExtra(EXTRA_URLS); - String name = intent.getStringExtra(EXTRA_NAME); - String location = intent.getStringExtra(EXTRA_LOCATION); - int threads = intent.getIntExtra(EXTRA_THREADS, 1); - char kind = intent.getCharExtra(EXTRA_KIND, '?'); - String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME); - String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS); - String source = intent.getStringExtra(EXTRA_SOURCE); - long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0); - - handleConnectivityState(true);// first check the actual network status - - mHandler.post(() -> mManager.startMission(urls, location, name, kind, threads, source, psName, psArgs, nearLength)); - + mHandler.post(() -> startMission(intent)); } else if (downloadDoneNotification != null) { if (action.equals(ACTION_RESET_DOWNLOAD_FINISHED) || action.equals(ACTION_OPEN_DOWNLOADS_FINISHED)) { downloadDoneCount = 0; @@ -264,12 +257,12 @@ public class DownloadManagerService extends Service { @Override public IBinder onBind(Intent intent) { int permissionCheck; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { - permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE); - if (permissionCheck == PermissionChecker.PERMISSION_DENIED) { - Toast.makeText(this, "Permission denied (read)", Toast.LENGTH_SHORT).show(); - } - } +// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { +// permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE); +// if (permissionCheck == PermissionChecker.PERMISSION_DENIED) { +// Toast.makeText(this, "Permission denied (read)", Toast.LENGTH_SHORT).show(); +// } +// } permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE); if (permissionCheck == PermissionChecker.PERMISSION_DENIED) { @@ -284,8 +277,8 @@ public class DownloadManagerService extends Service { switch (msg.what) { case MESSAGE_FINISHED: - notifyMediaScanner(mission.getDownloadedFile()); - notifyFinishedDownload(mission.name); + notifyMediaScanner(mission.storage.getUri()); + notifyFinishedDownload(mission.storage.getName()); mManager.setFinished(mission); handleConnectivityState(false); updateForegroundState(mManager.runMissions()); @@ -344,7 +337,7 @@ public class DownloadManagerService extends Service { if (key.equals(getString(R.string.downloads_maximum_retry))) { try { 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) { mManager.mPrefMaxRetry = 0; } @@ -353,6 +346,12 @@ public class DownloadManagerService extends Service { 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.downloads_storage_api))) { + setupStorageAPI(false); + } else if (key.equals(getString(R.string.download_path_video_key))) { + loadMainStorage(key, DownloadManager.TAG_VIDEO, false); + } else if (key.equals(getString(R.string.download_path_audio_key))) { + loadMainStorage(key, DownloadManager.TAG_AUDIO, false); } } @@ -370,43 +369,61 @@ public class DownloadManagerService extends Service { mForeground = state; } - 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) { Intent intent = new Intent(context, DownloadManagerService.class); intent.setAction(Intent.ACTION_RUN); intent.putExtra(EXTRA_URLS, urls); - intent.putExtra(EXTRA_NAME, name); - intent.putExtra(EXTRA_LOCATION, location); + intent.putExtra(EXTRA_PATH, storage.getUri()); intent.putExtra(EXTRA_KIND, kind); intent.putExtra(EXTRA_THREADS, threads); intent.putExtra(EXTRA_SOURCE, source); intent.putExtra(EXTRA_POSTPROCESSING_NAME, psName); intent.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs); intent.putExtra(EXTRA_NEAR_LENGTH, nearLength); + intent.putExtra(EXTRA_MAIN_STORAGE_TAG, storage.getTag()); context.startService(intent); } - public static void checkForRunningMission(Context context, String location, String name, DMChecker checker) { - Intent intent = new Intent(); - intent.setClass(context, DownloadManagerService.class); - context.startService(intent); + public void startMission(Intent intent) { + String[] urls = intent.getStringArrayExtra(EXTRA_URLS); + Uri path = intent.getParcelableExtra(EXTRA_PATH); + int threads = intent.getIntExtra(EXTRA_THREADS, 1); + char kind = intent.getCharExtra(EXTRA_KIND, '?'); + String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME); + String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS); + String source = intent.getStringExtra(EXTRA_SOURCE); + long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0); + String tag = intent.getStringExtra(EXTRA_MAIN_STORAGE_TAG); - context.bindService(intent, new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName cname, IBinder service) { - try { - ((DMBinder) service).getDownloadManager().checkForRunningMission(location, name, checker); - } catch (Exception err) { - Log.w(TAG, "checkForRunningMission() callback is defective", err); - } + StoredFileHelper storage; + try { + storage = new StoredFileHelper(this, path, tag); + } catch (IOException e) { + throw new RuntimeException(e);// this never should happen + } - context.unbindService(this); - } + final DownloadMission mission = new DownloadMission(urls, storage, kind, Postprocessing.getAlgorithm(psName, psArgs)); + mission.threadCount = threads; + mission.source = source; + mission.nearLength = nearLength; - @Override - public void onServiceDisconnected(ComponentName name) { - } - }, Context.BIND_AUTO_CREATE); + handleConnectivityState(true);// first check the actual network status + + mManager.startMission(mission); } public void notifyFinishedDownload(String name) { @@ -471,12 +488,12 @@ public class DownloadManagerService extends Service { if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { downloadFailedNotification.setContentTitle(getString(R.string.app_name)); 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 { downloadFailedNotification.setContentTitle(getString(R.string.download_failed)); - downloadFailedNotification.setContentText(mission.name); + downloadFailedNotification.setContentText(mission.storage.getName()); downloadFailedNotification.setStyle(new NotificationCompat.BigTextStyle() - .bigText(mission.name)); + .bigText(mission.storage.getName())); } mNotificationManager.notify(id, downloadFailedNotification.build()); @@ -508,16 +525,81 @@ public class DownloadManagerService extends Service { mLockAcquired = acquire; } + private void setupStorageAPI(boolean acquire) { + loadMainStorage(getString(R.string.download_path_audio_key), DownloadManager.TAG_VIDEO, acquire); + loadMainStorage(getString(R.string.download_path_video_key), DownloadManager.TAG_AUDIO, acquire); + } + + void loadMainStorage(String prefKey, String tag, boolean acquire) { + String path = mPrefs.getString(prefKey, null); + + final String JAVA_IO = getString(R.string.downloads_storage_api_default); + boolean useJavaIO = JAVA_IO.equals(mPrefs.getString(getString(R.string.downloads_storage_api), JAVA_IO)); + + final String defaultPath; + if (tag.equals(DownloadManager.TAG_VIDEO)) + defaultPath = Environment.DIRECTORY_MOVIES; + else// if (tag.equals(DownloadManager.TAG_AUDIO)) + defaultPath = Environment.DIRECTORY_MUSIC; + + StoredDirectoryHelper mainStorage; + if (path == null || path.isEmpty()) { + mainStorage = useJavaIO ? new StoredDirectoryHelper(defaultPath, tag) : null; + } else { + + if (path.charAt(0) == File.separatorChar) { + Log.i(TAG, "Migrating old save path: " + path); + + useJavaIO = true; + path = Uri.fromFile(new File(path)).toString(); + + mPrefs.edit().putString(prefKey, path).apply(); + } + + if (useJavaIO) { + mainStorage = new StoredDirectoryHelper(path, tag); + } else { + + // tree api is not available in older versions + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + mainStorage = null; + } else { + try { + mainStorage = new StoredDirectoryHelper(this, Uri.parse(path), tag); + if (acquire) mainStorage.acquirePermissions(); + } catch (IOException e) { + Log.e(TAG, "Failed to load the storage of " + tag + " from path: " + path, e); + mainStorage = null; + } + } + } + } + + if (tag.equals(DownloadManager.TAG_VIDEO)) + mManager.mMainStorageVideo = mainStorage; + else// if (tag.equals(DownloadManager.TAG_AUDIO)) + mManager.mMainStorageAudio = mainStorage; + } //////////////////////////////////////////////////////////////////////////////////////////////// // Wrappers for DownloadManager //////////////////////////////////////////////////////////////////////////////////////////////// - public class DMBinder extends Binder { + public class DownloadManagerBinder extends Binder { public DownloadManager getDownloadManager() { return mManager; } + @Nullable + public StoredDirectoryHelper getMainStorageVideo() { + return mManager.mMainStorageVideo; + } + + @Nullable + public StoredDirectoryHelper getMainStorageAudio() { + return mManager.mMainStorageAudio; + } + public void addMissionEventListener(Handler handler) { manageObservers(handler, true); } @@ -548,10 +630,4 @@ public class DownloadManagerService extends Service { } - public interface DMChecker { - void callback(MissionCheck result); - } - - public enum MissionCheck {None, Pending, PendingRunning, Finished} - } diff --git a/app/src/main/java/us/shandian/giga/service/MissionState.java b/app/src/main/java/us/shandian/giga/service/MissionState.java new file mode 100644 index 000000000..2d7802ff5 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/service/MissionState.java @@ -0,0 +1,5 @@ +package us.shandian.giga.service; + +public enum MissionState { + None, Pending, PendingRunning, Finished +} diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index cada3aeb8..4d80588e0 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -8,7 +8,6 @@ import android.content.Intent; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; -import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -49,6 +48,7 @@ import java.util.Collections; import us.shandian.giga.get.DownloadMission; 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.DownloadManagerService; import us.shandian.giga.ui.common.Deleter; @@ -69,6 +69,7 @@ import static us.shandian.giga.get.DownloadMission.ERROR_PERMISSION_DENIED; import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING; import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_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_UNKNOWN_EXCEPTION; import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST; @@ -97,8 +98,9 @@ public class MissionAdapter extends Adapter { private MenuItem mStartButton; private MenuItem mPauseButton; private View mEmptyMessage; + private RecoverHelper mRecover; - public MissionAdapter(Context context, DownloadManager downloadManager, View emptyMessage) { + public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage) { mContext = context; mDownloadManager = downloadManager; mDeleter = null; @@ -156,7 +158,11 @@ public class MissionAdapter extends Adapter { if (h.item.mission instanceof DownloadMission) { 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(); @@ -189,10 +195,10 @@ public class MissionAdapter extends Adapter { ViewHolderItem h = (ViewHolderItem) view; 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.name.setText(item.mission.name); + h.name.setText(item.mission.storage.getName()); h.progress.setColors(Utility.getBackgroundForFileType(mContext, type), Utility.getForegroundForFileType(mContext, type)); @@ -273,7 +279,7 @@ public class MissionAdapter extends Adapter { long length = mission.getLength(); int state; - if (mission.isPsFailed()) { + if (mission.isPsFailed() || mission.errCode == ERROR_POSTPROCESSING_HOLD) { state = 0; } else if (!mission.running) { state = mission.enqueued ? 1 : 2; @@ -334,11 +340,17 @@ public class MissionAdapter extends Adapter { if (BuildConfig.DEBUG) Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider"); - Uri uri = FileProvider.getUriForFile( - mContext, - BuildConfig.APPLICATION_ID + ".provider", - mission.getDownloadedFile() - ); + Uri uri; + + if (mission.storage.isDirect()) { + uri = FileProvider.getUriForFile( + mContext, + BuildConfig.APPLICATION_ID + ".provider", + mission.storage.getIOFile() + ); + } else { + uri = mission.storage.getUri(); + } Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); @@ -366,13 +378,13 @@ public class MissionAdapter extends Adapter { Intent intent = new Intent(Intent.ACTION_SEND); intent.setType(resolveMimeType(mission)); - intent.putExtra(Intent.EXTRA_STREAM, mission.getDownloadedFile().toURI()); + intent.putExtra(Intent.EXTRA_STREAM, mission.storage.getUri()); mContext.startActivity(Intent.createChooser(intent, null)); } private static String resolveMimeType(@NonNull Mission mission) { - String ext = Utility.getFileExt(mission.getDownloadedFile().getName()); + String ext = Utility.getFileExt(mission.storage.getName()); if (ext == null) return DEFAULT_MIME_TYPE; String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1)); @@ -381,7 +393,7 @@ public class MissionAdapter extends Adapter { } private boolean checkInvalidFile(@NonNull Mission mission) { - if (mission.getDownloadedFile().exists()) return false; + if (mission.storage.existsAsFile()) return false; Toast.makeText(mContext, R.string.missing_file, Toast.LENGTH_SHORT).show(); return true; @@ -462,6 +474,8 @@ public class MissionAdapter extends Adapter { 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; default: if (mission.errCode >= 100 && mission.errCode < 600) { msgEx = "HTTP " + mission.errCode; @@ -490,7 +504,7 @@ public class MissionAdapter extends Adapter { } builder.setNegativeButton(android.R.string.ok, (dialog, which) -> dialog.cancel()) - .setTitle(mission.name) + .setTitle(mission.storage.getName()) .create() .show(); } @@ -539,6 +553,10 @@ public class MissionAdapter extends Adapter { updateProgress(h); return true; case R.id.retry: + if (mission.hasInvalidStorage()) { + mRecover.tryRecover(mission); + return true; + } mission.psContinue(true); return true; case R.id.cancel: @@ -561,7 +579,7 @@ public class MissionAdapter extends Adapter { return true; case R.id.md5: 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; case R.id.source: /*Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(h.item.mission.source)); @@ -641,19 +659,38 @@ public class MissionAdapter extends Adapter { } - public void deleterDispose(Bundle bundle) { - if (mDeleter != null) mDeleter.dispose(bundle); + public void deleterDispose(boolean commitChanges) { + if (mDeleter != null) mDeleter.dispose(commitChanges); } - public void deleterLoad(Bundle bundle, View view) { + public void deleterLoad(View view) { 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() { if (mDeleter != null) mDeleter.resume(); } + public void recoverMission(DownloadMission mission, StoredFileHelper newStorage) { + for (ViewHolderItem h : mPendingDownloadsItems) { + if (mission != h.item.mission) continue; + + mission.changeStorage(newStorage); + mission.errCode = DownloadMission.ERROR_NOTHING; + mission.errObject = null; + + h.status.setText(UNDEFINED_PROGRESS); + h.state = -1; + h.size.setText(Utility.formatBytes(mission.getLength())); + h.progress.setMarquee(true); + + mDownloadManager.resumeMission(mission); + return; + } + + } + private boolean mUpdaterRunning = false; private final Runnable rUpdater = this::updater; @@ -695,6 +732,10 @@ public class MissionAdapter extends Adapter { return Float.isNaN(value) || Float.isInfinite(value); } + public void setRecover(@NonNull RecoverHelper callback) { + mRecover = callback; + } + class ViewHolderItem extends RecyclerView.ViewHolder { DownloadManager.MissionItem item; @@ -780,7 +821,11 @@ public class MissionAdapter extends Adapter { DownloadMission mission = item.mission instanceof DownloadMission ? (DownloadMission) item.mission : null; if (mission != null) { - if (mission.isPsRunning()) { + if (mission.hasInvalidStorage()) { + retry.setEnabled(true); + delete.setEnabled(true); + showError.setEnabled(true); + } else if (mission.isPsRunning()) { switch (mission.errCode) { case ERROR_INSUFFICIENT_STORAGE: case ERROR_POSTPROCESSING_HOLD: @@ -838,7 +883,7 @@ public class MissionAdapter extends Adapter { } - static class ChecksumTask extends AsyncTask { + static class ChecksumTask extends AsyncTask { ProgressDialog progressDialog; WeakReference weakReference; @@ -861,8 +906,8 @@ public class MissionAdapter extends Adapter { } @Override - protected String doInBackground(String... params) { - return Utility.checksum(params[0], params[1]); + protected String doInBackground(Object... params) { + return Utility.checksum((StoredFileHelper) params[0], (String) params[1]); } @Override @@ -889,4 +934,8 @@ public class MissionAdapter extends Adapter { } } + public interface RecoverHelper { + void tryRecover(DownloadMission mission); + } + } diff --git a/app/src/main/java/us/shandian/giga/ui/common/Deleter.java b/app/src/main/java/us/shandian/giga/ui/common/Deleter.java index 6407ab019..573bead94 100644 --- a/app/src/main/java/us/shandian/giga/ui/common/Deleter.java +++ b/app/src/main/java/us/shandian/giga/ui/common/Deleter.java @@ -3,8 +3,6 @@ package us.shandian.giga.ui.common; import android.content.Context; import android.content.Intent; import android.graphics.Color; -import android.net.Uri; -import android.os.Bundle; import android.os.Handler; import android.support.design.widget.Snackbar; import android.view.View; @@ -23,8 +21,6 @@ public class Deleter { private static final int TIMEOUT = 5000;// ms private static final int DELAY = 350;// 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 ArrayList items; @@ -41,7 +37,7 @@ public class Deleter { private final Runnable rNext; 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; mContext = c; mAdapter = a; @@ -55,27 +51,6 @@ public class Deleter { rCommit = this::commit; items = new ArrayList<>(2); - - if (b != null) { - String[] names = b.getStringArray(BUNDLE_NAMES); - String[] locations = b.getStringArray(BUNDLE_LOCATIONS); - - if (names == null || locations == null) return; - if (names.length < 1 || locations.length < 1) return; - if (names.length != locations.length) return; - - items.ensureCapacity(names.length); - - for (int j = 0; j < locations.length; j++) { - Mission mission = mDownloadManager.getAnyMission(locations[j], names[j]); - if (mission == null) continue; - - items.add(mission); - mIterator.hide(mission); - } - - if (items.size() > 0) resume(); - } } public void append(Mission item) { @@ -104,7 +79,7 @@ public class Deleter { private void next() { 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.setAction(R.string.undo, s -> forget()); @@ -125,7 +100,7 @@ public class Deleter { mDownloadManager.deleteMission(mission); 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; } @@ -151,27 +126,14 @@ public class Deleter { mHandler.postDelayed(rShow, DELAY_RESUME); } - public void dispose(Bundle bundle) { + public void dispose(boolean commitChanges) { if (items.size() < 1) return; pause(); - if (bundle == null) { - for (Mission mission : items) mDownloadManager.deleteMission(mission); - items = null; - return; - } + if (!commitChanges) return; - String[] names = new String[items.size()]; - String[] locations = new String[items.size()]; - - for (int i = 0; i < items.size(); i++) { - Mission mission = items.get(i); - names[i] = mission.name; - locations[i] = mission.location; - } - - bundle.putStringArray(BUNDLE_NAMES, names); - bundle.putStringArray(BUNDLE_LOCATIONS, locations); + for (Mission mission : items) mDownloadManager.deleteMission(mission); + items = null; } } diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index a3786a5e6..82ab777b0 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -1,7 +1,6 @@ package us.shandian.giga.ui.fragment; import android.app.Activity; -import android.app.Fragment; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -10,6 +9,7 @@ import android.content.SharedPreferences; import android.os.Bundle; import android.os.IBinder; import android.preference.PreferenceManager; +import android.support.v4.app.Fragment; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; @@ -18,18 +18,24 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.Toast; import org.schabi.newpipe.R; 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.DownloadManagerService; -import us.shandian.giga.service.DownloadManagerService.DMBinder; +import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder; import us.shandian.giga.ui.adapter.MissionAdapter; public class MissionsFragment extends Fragment { private static final int SPAN_SIZE = 2; + private static final int REQUEST_DOWNLOAD_PATH_SAF = 0x1230; private SharedPreferences mPrefs; private boolean mLinear; @@ -45,24 +51,32 @@ public class MissionsFragment extends Fragment { private LinearLayoutManager mLinearManager; private Context mContext; - private DMBinder mBinder; - private Bundle mBundle; + private DownloadManagerBinder mBinder; private boolean mForceUpdate; + private DownloadMission unsafeMissionTarget = null; + private ServiceConnection mConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder binder) { - mBinder = (DownloadManagerService.DMBinder) binder; + mBinder = (DownloadManagerBinder) binder; mBinder.clearDownloadNotifications(); mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty); - mAdapter.deleterLoad(mBundle, getView()); + mAdapter.deleterLoad(getView()); + + mAdapter.setRecover(mission -> + StoredFileHelper.requestSafWithFileCreation( + MissionsFragment.this, + REQUEST_DOWNLOAD_PATH_SAF, + mission.storage.getName(), + mission.storage.getType() + ) + ); setAdapterButtons(); - mBundle = null; - mBinder.addMissionEventListener(mAdapter.getMessenger()); mBinder.enableNotifications(false); @@ -84,9 +98,6 @@ public class MissionsFragment extends Fragment { mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); mLinear = mPrefs.getBoolean("linear", false); - //mContext = getActivity().getApplicationContext(); - mBundle = savedInstanceState; - // Bind the service mContext.bindService(new Intent(mContext, DownloadManagerService.class), mConnection, Context.BIND_AUTO_CREATE); @@ -148,7 +159,7 @@ public class MissionsFragment extends Fragment { mBinder.removeMissionEventListener(mAdapter.getMessenger()); mBinder.enableNotifications(true); mContext.unbindService(mConnection); - mAdapter.deleterDispose(null); + mAdapter.deleterDispose(true); mBinder = null; mAdapter = null; @@ -178,10 +189,12 @@ public class MissionsFragment extends Fragment { 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: @@ -231,7 +244,7 @@ public class MissionsFragment extends Fragment { super.onSaveInstanceState(outState); if (mAdapter != null) { - mAdapter.deleterDispose(outState); + mAdapter.deleterDispose(false); mForceUpdate = true; mBinder.removeMissionEventListener(mAdapter.getMessenger()); } @@ -260,4 +273,22 @@ public class MissionsFragment extends Fragment { if (mAdapter != null) mAdapter.onPaused(); if (mBinder != null) mBinder.enableNotifications(true); } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode != REQUEST_DOWNLOAD_PATH_SAF || resultCode != Activity.RESULT_OK) return; + + if (unsafeMissionTarget == null || data.getData() == null) { + return;// unsafeMissionTarget cannot be null + } + + try { + StoredFileHelper storage = new StoredFileHelper(mContext, data.getData(), unsafeMissionTarget.storage.getTag()); + mAdapter.recoverMission(unsafeMissionTarget, storage); + } catch (IOException e) { + Toast.makeText(mContext, R.string.general_error, Toast.LENGTH_LONG).show(); + } + } } diff --git a/app/src/main/java/us/shandian/giga/util/Utility.java b/app/src/main/java/us/shandian/giga/util/Utility.java index e5149cf9b..d77e598d8 100644 --- a/app/src/main/java/us/shandian/giga/util/Utility.java +++ b/app/src/main/java/us/shandian/giga/util/Utility.java @@ -12,11 +12,11 @@ import android.support.v4.content.ContextCompat; import android.widget.Toast; import org.schabi.newpipe.R; +import org.schabi.newpipe.streams.io.SharpStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; @@ -25,7 +25,8 @@ import java.io.Serializable; import java.net.HttpURLConnection; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.Locale; + +import us.shandian.giga.io.StoredFileHelper; public class Utility { @@ -206,7 +207,7 @@ public class Utility { 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; try { @@ -215,11 +216,11 @@ public class Utility { throw new RuntimeException(e); } - FileInputStream i; + SharpStream i; try { - i = new FileInputStream(path); - } catch (FileNotFoundException e) { + i = source.getStream(); + } catch (Exception e) { throw new RuntimeException(e); } @@ -247,15 +248,15 @@ public class Utility { } @SuppressWarnings("ResultOfMethodCallIgnored") - public static boolean mkdir(File path, boolean allDirs) { - if (path.exists()) return true; + public static boolean mkdir(File p, boolean allDirs) { + if (p.exists()) return true; if (allDirs) - path.mkdirs(); + p.mkdirs(); else - path.mkdir(); + p.mkdir(); - return path.exists(); + return p.exists(); } public static long getContentLength(HttpURLConnection connection) { diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index b7a0ec7b0..dbf015c87 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -462,12 +462,12 @@ %s أنتهى التحميل إنشاء اسم فريد الكتابة فوق - يوجد ملف تحميل بهذا الاسم موجود مسبقاً + يوجد ملف تحميل بهذا الاسم موجود مسبقاً هنالك تحميل قيد التقدم بهذا الاسم إظهار خطأ كود - لا يمكن إنشاء الملف - لا يمكن إنشاء المجلد الوجهة + لا يمكن إنشاء الملف + لا يمكن إنشاء المجلد الوجهة تم رفضها من قبل النظام فشل اتصال الأمن تعذر العثور على الخادم diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 4285a73dc..f606281f4 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -432,8 +432,8 @@ Genera un nom únic Mostra l\'error Codi - No es pot crear el fitxer - No es pot crear la carpeta de destinació + No es pot crear el fitxer + No es pot crear la carpeta de destinació Atura Esdeveniments Notificacions de noves versions del NewPipe diff --git a/app/src/main/res/values-cmn/strings.xml b/app/src/main/res/values-cmn/strings.xml index f6c8e3e4a..73eb43c36 100644 --- a/app/src/main/res/values-cmn/strings.xml +++ b/app/src/main/res/values-cmn/strings.xml @@ -437,11 +437,11 @@ %s已下载完毕 生成独特的名字 覆写 - 同名的已下载文件已经存在 + 同名的已下载文件已经存在 同名下载进行中 显示错误 代码 - 无法创建该文件 + 无法创建该文件 系统拒绝此批准 安全连接失败 找不到服务器 @@ -464,7 +464,7 @@ 网格 切换视图 NewPipe 更新可用! - 无法创建目标文件夹 + 无法创建目标文件夹 服务器不接受多线程下载, 请重试使用 @string/msg_threads = 1 请求范围无法满足 继续进行%s个待下载转移 diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 4dc81f9c2..92535310e 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -372,8 +372,8 @@ Der er en download i gang med dette navn Vis fejl Kode - Filen kan ikke oprettes - Destinationsmappen kan ikke oprettes + Filen kan ikke oprettes + Destinationsmappen kan ikke oprettes Adgang nægtet af systemet Sikker forbindelse fejlede Kunne ikke finde serveren diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index dada4e80f..ef2789846 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -448,12 +448,12 @@ %s heruntergeladen Eindeutigen Namen erzeugen Überschreiben - Eine heruntergeladene Datei dieses Namens existiert bereits + Eine heruntergeladene Datei dieses Namens existiert bereits Eine Datei dieses Namens wird gerade heruntergeladen Fehler anzeigen Code - Die Datei kann nicht erstellt werden - Der Zielordner kann nicht erstellt werden + Die Datei kann nicht erstellt werden + Der Zielordner kann nicht erstellt werden System verweigert den Zugriff Sichere Verbindung fehlgeschlagen Der Server konnte nicht gefunden werden diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 818efc74c..eee110474 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -14,7 +14,7 @@ Compartir con Elegir navegador rotación - Ruta de descarga de vídeo + Carpeta de descarga de vídeo Ruta para almacenar los vídeos descargados Introducir directorio de descargas para vídeos Resolución por defecto de vídeo @@ -40,7 +40,7 @@ (Experimental) Forzar la descarga a través de Tor para una mayor privacidad (transmisión de vídeos aún no compatible). No se puede crear la carpeta de descarga \'%1$s\' Carpeta de descarga creada \'%1$s\' - Los audios descargados se almacenan aquí + Ruta para almacenar los audios descargados Introducir ruta de descarga para archivos de audio Bloqueado por GEMA Carpeta de descarga de audio @@ -418,7 +418,9 @@ abrir en modo popup Generar nombre único Sobrescribir - Ya existe un archivo descargado con este nombre + Ya existe un archivo con este nombre + Ya existe un archivo descargado con este nombre + No se puede sobrescribir el archivo Hay una descarga en curso con este nombre Hay una descarga pendiente con este nombre @@ -440,8 +442,8 @@ abrir en modo popup Mostrar error Codigo - No se puede crear la carpeta de destino - No se puede crear el archivo + No se puede crear la carpeta de destino + No se puede crear el archivo Permiso denegado por el sistema Fallo la conexión segura No se pudo encontrar el servidor @@ -453,6 +455,19 @@ abrir en modo popup Fallo el post-procesado NewPipe se cerro mientras se trabajaba en el archivo No hay suficiente espacio disponible en el dispositivo + Se perdió el progreso porque el archivo fue eliminado + + API de almacenamiento + Seleccione que API utilizar para almacenar las descargas + + Framework de acceso a almacenamiento + Java I/O + + Guardar como… + + No es posible descargar a una tarjeta SD externa. \¿Restablecer la ubicación de la carpeta de descarga\? + + Seleccione los directorios de descarga Desuscribirse Nueva pestaña diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 095bbfe5e..2868528e9 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -446,12 +446,12 @@ %s deskarga amaituta Sortu izen bakana Gainidatzi - Badago izen bera duen deskargatutako fitxategi bat + Badago izen bera duen deskargatutako fitxategi bat Badago izen bera duen deskarga bat abian Erakutsi errorea Kodea - Ezin da fitxategia sortu - Ezin da helburu karpeta sortu + Ezin da fitxategia sortu + Ezin da helburu karpeta sortu Sistemak baimena ukatu du Konexio seguruak huts egin du Ezin izan da zerbitzaria aurkitu diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 9edee2411..41569ff0c 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -451,12 +451,12 @@ %s הורדות הסתיימו יצירת שם ייחודי שכתוב - כבר קיים קובץ בשם הזה + כבר קיים קובץ בשם הזה אחת ההורדות הפעילות כבר נושאת את השם הזה הצגת שגיאה קוד - לא ניתן ליצור את הקובץ - לא ניתן ליצור את תיקיית היעד + לא ניתן ליצור את הקובץ + לא ניתן ליצור את תיקיית היעד ההרשאה נדחתה על ידי המערכת החיבור המאובטח נכשל לא ניתן למצוא את השרת diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 56f31eac6..31801434b 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -447,12 +447,12 @@ %s unduhan selesai Hasilkan nama unik Timpa - File yang diunduh dengan nama ini sudah ada + File yang diunduh dengan nama ini sudah ada Ada unduhan yang sedang berlangsung dengan nama ini Tunjukkan kesalahan Kode - File tidak dapat dibuat - Folder tujuan tidak dapat dibuat + File tidak dapat dibuat + Folder tujuan tidak dapat dibuat Izin ditolak oleh sistem Koneksi aman gagal Tidak dapat menemukan server diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 9bccc11df..4ff8de734 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -449,12 +449,12 @@ %s download finiti Genera un nome unico Sovrascrivi - Esiste già un file scaricato con lo stesso nome + Esiste già un file scaricato con lo stesso nome C\'è un download in progresso con questo nome Mostra errore Codice - Impossibile creare il file - Impossibile creare la cartella di destinazione + Impossibile creare il file + Impossibile creare la cartella di destinazione Permesso negato dal sistema Connessione sicura fallita Impossibile trovare il server diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 124c3580b..78a20b1ab 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -365,10 +365,10 @@ 自動生成 アプリの再起動後、設定した字幕設定が反映されます 何もありません - 保存したエクスポートファイルからYouTubeの購読をインポート: -\n -\n1. このURLを開きます: %1$s -\n2. ログインしていなければログインします + 保存したエクスポートファイルからYouTubeの購読をインポート: +\n +\n1. このURLを開きます: %1$s +\n2. ログインしていなければログインします \n3. ダウンロードが始まります (これがエクスポートファイルです) リセット 同意する @@ -391,8 +391,8 @@ \n3. 必要に応じてログインします \n4. リダイレクトされたプロファイル URL をコピーします。 あなたのID, soundcloud.com/あなたのid - この操作により通信料金が増えることがあります。ご注意ください。 -\n + この操作により通信料金が増えることがあります。ご注意ください。 +\n \n続行しますか\? 再生速度を変更 速度と音程を連動せずに変更 (歪むかもしれません) diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index 7c8b56bec..ced8235f7 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -443,12 +443,12 @@ %s muat turun selesai Menjana nama yang unik Timpa - Fail yang dimuat turun dengan nama ini sudah wujud + Fail yang dimuat turun dengan nama ini sudah wujud Terdapat muat turun yang sedang berjalan dengan nama ini Tunjukkan kesilapan Kod - Fail tidak boleh dibuat - Folder destinasi tidak boleh dibuat + Fail tidak boleh dibuat + Folder destinasi tidak boleh dibuat Kebenaran ditolak oleh sistem Sambungan selamat gagal Tidak dapat mencari server diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index a806df31a..2f5d19c67 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -526,12 +526,12 @@ %s nedlastinger fullført Generer unikt navn Overskriv - Nedlastet fil ved dette navnet finnes allerede + Nedlastet fil ved dette navnet finnes allerede Nedlasting med dette navnet underveis allerede Vis feil Kode - Filen kan ikke opprettes - Målmappen kan ikke opprettes + Filen kan ikke opprettes + Målmappen kan ikke opprettes Tilgang nektet av systemet Sikker tilkobling mislyktes Fant ikke tjeneren diff --git a/app/src/main/res/values-nl-rBE/strings.xml b/app/src/main/res/values-nl-rBE/strings.xml index 51a30aa36..44b2ef6ab 100644 --- a/app/src/main/res/values-nl-rBE/strings.xml +++ b/app/src/main/res/values-nl-rBE/strings.xml @@ -445,12 +445,12 @@ %s downloads voltooid Unieke naam genereren Overschrijven - Der bestaat al een gedownload bestand met deze naam + Der bestaat al een gedownload bestand met deze naam Der is al een download met deze naam bezig Foutmelding weergeven Code - Het bestand kan niet aangemaakt worden - De doelmap kan niet aangemaakt worden + Het bestand kan niet aangemaakt worden + De doelmap kan niet aangemaakt worden Toelating geweigerd door het systeem Beveiligde verbinding is mislukt Kon de server niet vinden diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 04b47c4c8..96de68b57 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -449,12 +449,12 @@ %s downloads voltooid Genereer een unieke naam Overschrijven - Er bestaat al een gedownload bestand met deze naam + Er bestaat al een gedownload bestand met deze naam Er is een download aan de gang met deze naam Toon foutmelding Code - Het bestand kan niet worden gemaakt - De doelmap kan niet worden gemaakt + Het bestand kan niet worden gemaakt + De doelmap kan niet worden gemaakt Toestemming door het systeem geweigerd Beveiligde connectie is mislukt Kon de server niet vinden diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 4fedc93d9..29070990f 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -446,12 +446,12 @@ %s pobieranie zostało zakończone Wygeneruj unikalną nazwę Zastąp - Pobrany plik o tej nazwie już istnieje + Pobrany plik o tej nazwie już istnieje Trwa pobieranie z tą nazwą Pokaż błąd Kod - Nie można utworzyć pliku - Nie można utworzyć folderu docelowego + Nie można utworzyć pliku + Nie można utworzyć folderu docelowego Odmowa dostępu do systemu Bezpieczne połączenie nie powiodło się Nie można znaleźć serwera diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 798131f37..8a16b752d 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -446,12 +446,12 @@ abrir em modo popup %s downloads terminados Gerar nome único "Sobrescrever " - Um arquivo baixado com esse nome já existe + Um arquivo baixado com esse nome já existe Existe um download em progresso com esse nome Mostrar erro Código - O arquivo não pode ser criado - A pasta de destino não pode ser criada + O arquivo não pode ser criado + A pasta de destino não pode ser criada Permissão negada pelo sistema "Falha na conexão segura " Não foi possível encontrar o servidor diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 9db2e401a..a86c5b809 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -442,12 +442,12 @@ %s descargas terminadas Gerar nome único Sobrescrever - Um ficheiro descarregado com este nome já existe + Um ficheiro descarregado com este nome já existe Já existe uma descarga em curso com este nome Mostrar erro Código - O ficheiro não pode ser criado - A pasta de destino não pode ser criada + O ficheiro não pode ser criado + A pasta de destino não pode ser criada Permissão negada pelo sistema Ligação segura falhou Não foi possível encontrar o servidor diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index d4bf5d803..620ca5619 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -442,12 +442,12 @@ Действие запрещено системой Ошибка загрузки Перезаписать - Файл с таким именем уже существует + Файл с таким именем уже существует Загрузка с таким именем уже выполняется Показать текст ошибки Код - Файл не может быть создан - Папка назначения не может быть создана + Папка назначения не может быть создана + Файл не может быть создан Доступ запрещен системой Сервер не найден "Сервер не поддерживает многопотоковую загрузку, попробуйте с @string/msg_threads = 1" diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 5209fe250..e518a1c0f 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -449,12 +449,12 @@ %s indirme bitti Benzersiz ad oluştur Üzerine yaz - Bu ada sahip indirilen bir dosya zaten var + Bu ada sahip indirilen bir dosya zaten var Bu ad ile devam eden bir indirme var Hatayı göster Kod - Dosya oluşturulamıyor - Hedef klasör oluşturulamıyor + Dosya oluşturulamıyor + Hedef klasör oluşturulamıyor İzin sistem tarafından reddedildi Güvenli bağlantı başarısız Sunucu bulunamadı diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index b1899622f..ff247c579 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -440,11 +440,11 @@ %s tải về đã xong Tạo tên riêng biệt Ghi đè - Có một tệp đã tải về trùng tên + Có một tệp đã tải về trùng tên Có một tệp trùng tên đang tải về Hiện lỗi - Không thể tạo tệp - Không thể tạo thư mục đích + Không thể tạo tệp + Không thể tạo thư mục đích Quyền bị từ chối bởi hệ thống Không thể tạo kết nối an toàn Không thể tìm máy chủ diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 441061657..0194418cf 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -445,12 +445,12 @@ %s 個下載已結束 生成獨特的名稱 覆寫 - 已有此名稱的已下載檔案 + 已有此名稱的已下載檔案 已有此名稱的當案的下載正在進行 顯示錯誤 代碼 - 無法建立檔案 - 無法建立目的地資料夾 + 無法建立檔案 + 無法建立目的地資料夾 被系統拒絕的權限 安全連線失敗 找不到伺服器 diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 214a074c4..b2be3135b 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -11,7 +11,7 @@ saved_tabs_key - download_path + download_path download_path_audio use_external_video_player @@ -160,6 +160,21 @@ clear_play_history clear_search_history + downloads_storage_api + + + javaIO + + + SAF + javaIO + + + + @string/storage_access_framework_description + @string/java_io_description + + file_rename file_replacement_character diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index afc6afeb3..7907d2974 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -176,7 +176,7 @@ Error External storage unavailable - Downloading to external SD card not yet possible. Reset download folder location\? + Downloading to external SD card not possible. Reset download folder location\? Network error Could not load all thumbnails Could not decrypt video URL signature @@ -512,15 +512,17 @@ Generate unique name Overwrite - A downloaded file with this name already exists + A file with this name already exists + A downloaded file with this name already exists + cannot overwrite the file There is a download in progress with this name There is a pending download with this name Show error Code - The file can not be created - The destination folder can not be created + The file can not be created + The destination folder can not be created Permission denied by the system Secure connection failed Could not find the server @@ -532,6 +534,7 @@ Post-processing failed NewPipe was closed while working on the file No space left on device + Progress lost, because the file was deleted Clear finished downloads Continue your %s pending transfers from Downloads @@ -546,4 +549,14 @@ Start downloads Pause downloads + Storage API + Select which API use to store the downloads + + Storage Access Framework + Java I/O + + Save as… + + Select the downloads save path + \ No newline at end of file diff --git a/app/src/main/res/xml/download_settings.xml b/app/src/main/res/xml/download_settings.xml index 9f32e7f2f..bbb91acac 100644 --- a/app/src/main/res/xml/download_settings.xml +++ b/app/src/main/res/xml/download_settings.xml @@ -4,10 +4,26 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:title="@string/settings_category_downloads_title"> + + + + + From d00dc798f468cf1e9f47ad8703b866259ebfdd32 Mon Sep 17 00:00:00 2001 From: kapodamy Date: Tue, 9 Apr 2019 18:38:34 -0300 Subject: [PATCH 14/30] more SAF implementation * full support for Directory API (Android Lollipop or later) * best effort to handle any kind errors (missing file, revoked permissions, etc) and recover the download * implemented directory choosing * fix download database version upgrading * misc. cleanup * do not release permission on the old save path (if the user change the download directory) under SAF api --- .../newpipe/download/DownloadDialog.java | 202 ++++++++------ .../settings/DownloadSettingsFragment.java | 164 +++++++---- .../schabi/newpipe/streams/DataReader.java | 4 +- .../newpipe/streams/Mp4FromDashWriter.java | 8 +- .../schabi/newpipe/util/FilenameUtils.java | 26 +- .../giga/get/DownloadInitializer.java | 2 +- .../us/shandian/giga/get/DownloadMission.java | 41 ++- .../shandian/giga/get/DownloadRunnable.java | 8 +- .../us/shandian/giga/get/FinishedMission.java | 2 + .../java/us/shandian/giga/get/Mission.java | 14 +- .../giga/get/sqlite/FinishedMissionStore.java | 44 ++- .../shandian/giga/io/CircularFileWriter.java | 91 ++++-- .../us/shandian/giga/io/FileStreamSAF.java | 5 + .../giga/io/StoredDirectoryHelper.java | 263 +++++++++++------- .../us/shandian/giga/io/StoredFileHelper.java | 248 +++++++++++------ .../giga/postprocessing/Mp4FromDashMuxer.java | 2 +- .../giga/postprocessing/Postprocessing.java | 15 +- .../giga/postprocessing/WebMMuxer.java | 2 +- .../giga/service/DownloadManager.java | 171 ++++++------ .../giga/service/DownloadManagerService.java | 134 +++++---- .../giga/ui/adapter/MissionAdapter.java | 33 ++- .../giga/ui/fragment/MissionsFragment.java | 29 +- .../java/us/shandian/giga/util/Utility.java | 2 + app/src/main/res/values-es/strings.xml | 4 +- app/src/main/res/values/settings_keys.xml | 12 +- app/src/main/res/values/strings.xml | 3 +- app/src/main/res/xml/download_settings.xml | 6 - assets/db.dia | Bin 2520 -> 2508 bytes 28 files changed, 946 insertions(+), 589 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 4525c5988..8f4b569cd 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -15,12 +15,13 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.support.v4.app.DialogFragment; +import android.support.v4.provider.DocumentFile; import android.support.v7.app.AlertDialog; +import android.support.v7.view.menu.ActionMenuItemView; import android.support.v7.widget.Toolbar; import android.util.Log; import android.util.SparseArray; import android.view.LayoutInflater; -import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; @@ -177,9 +178,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck return; } - final Context context = getContext(); - if (context == null) - throw new RuntimeException("Context was null"); + context = getContext(); setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context)); Icepick.restoreInstanceState(this, savedInstanceState); @@ -321,11 +320,15 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck showFailedDialog(R.string.general_error); return; } - try { - continueSelectedDownload(new StoredFileHelper(getContext(), data.getData(), "")); - } catch (IOException e) { - showErrorActivity(e); + + 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()); } } @@ -337,14 +340,14 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck if (DEBUG) Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]"); boolean isLight = ThemeHelper.isLightThemeSelected(getActivity()); - okButton = toolbar.findViewById(R.id.okay); - okButton.setEnabled(false);// disable until the download service connection is done toolbar.setTitle(R.string.download_dialog_title); toolbar.setNavigationIcon(isLight ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp); toolbar.inflateMenu(R.menu.dialog_url); 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 -> { if (item.getItemId() == R.id.okay) { @@ -504,15 +507,17 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck StoredDirectoryHelper mainStorageAudio = null; StoredDirectoryHelper mainStorageVideo = null; DownloadManager downloadManager = null; - - MenuItem okButton = null; + ActionMenuItemView okButton = null; + Context context; private String getNameEditText() { - return nameEditText.getText().toString().trim(); + String str = nameEditText.getText().toString().trim(); + + return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str); } private void showFailedDialog(@StringRes int msg) { - new AlertDialog.Builder(getContext()) + new AlertDialog.Builder(context) .setMessage(msg) .setNegativeButton(android.R.string.ok, null) .create() @@ -521,7 +526,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck private void showErrorActivity(Exception e) { ErrorActivity.reportError( - getContext(), + context, Collections.singletonList(e), null, null, @@ -530,18 +535,14 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck } private void prepareSelectedDownload() { - final Context context = getContext(); StoredDirectoryHelper mainStorage; MediaFormat format; String mime; // first, build the filename and get the output folder (if possible) + // later, run a very very very large file checking logic - String filename = getNameEditText() + "."; - if (filename.isEmpty()) { - filename = FilenameUtils.createFilename(context, currentInfo.getName()); - } - filename += "."; + String filename = getNameEditText().concat("."); switch (radioStreamsGroup.getCheckedRadioButtonId()) { case R.id.audio_button: @@ -567,34 +568,33 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck } if (mainStorage == null) { - // this part is called if... - // older android version running with SAF preferred - // save path not defined (via download settings) + // This part is called if with SAF preferred: + // * older android version running + // * save path not defined (via download settings) StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_PATH_SAF, filename, mime); return; } // check for existing file with the same name - Uri result = mainStorage.findFile(filename); + checkSelectedDownload(mainStorage, mainStorage.findFile(filename), filename, mime); + } - if (result == null) { - // the file does not exists, create - StoredFileHelper storage = mainStorage.createFile(filename, mime); - if (storage == null || !storage.canWrite()) { - showFailedDialog(R.string.error_file_creation); - return; - } - - continueSelectedDownload(storage); - return; - } - - // the target filename is already use, try load + private void checkSelectedDownload(StoredDirectoryHelper mainStorage, Uri targetFile, String filename, String mime) { StoredFileHelper storage; + try { - storage = new StoredFileHelper(context, result, mime); - } catch (IOException e) { + 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; } @@ -618,6 +618,25 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck 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 + 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; @@ -625,49 +644,73 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck return; } - // handle user answer (overwrite or create another file with different name) - final String finalFilename = filename; - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.download_dialog_title) - .setMessage(msgBody) - .setPositiveButton(msgBtn, (dialog, which) -> { - dialog.dismiss(); - StoredFileHelper storageNew; - switch (state) { - case Finished: - case Pending: - downloadManager.forgetMission(storage); - case None: - // try take (or steal) the file permissions - try { - storageNew = new StoredFileHelper(context, result, mainStorage.getTag()); - if (storageNew.canWrite()) - continueSelectedDownload(storageNew); - else - showFailedDialog(R.string.error_file_creation); - } catch (IOException e) { - showErrorActivity(e); - } - break; - case PendingRunning: - // FIXME: createUniqueFile() is not tested properly - storageNew = mainStorage.createUniqueFile(finalFilename, mime); - if (storageNew == null) - showFailedDialog(R.string.error_file_creation); - else - continueSelectedDownload(storageNew); - break; + AlertDialog.Builder askDialog = new AlertDialog.Builder(context) + .setTitle(R.string.download_dialog_title) + .setMessage(msgBody) + .setNegativeButton(android.R.string.cancel, null); + final StoredFileHelper finalStorage = storage; + + + 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; + } + + askDialog.setPositiveButton(msgBtn, (dialog, which) -> { + dialog.dismiss(); + + StoredFileHelper storageNew; + switch (state) { + case Finished: + case Pending: + downloadManager.forgetMission(finalStorage); + case None: + if (targetFile == null) { + storageNew = mainStorage.createFile(filename, mime); + } else { + try { + // try take (or steal) the file + storageNew = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag()); + } catch (IOException e) { + Log.e(TAG, "Failed to take (or steal) the file in " + targetFile.toString()); + storageNew = null; + } } - }) - .setNegativeButton(android.R.string.cancel, null) - .create() - .show(); + + 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 continueSelectedDownload(@NonNull StoredFileHelper storage) { - final Context context = getContext(); - if (!storage.canWrite()) { showFailedDialog(R.string.permission_denied); return; @@ -678,7 +721,6 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck if (storage.length() > 0) storage.truncate(); } catch (IOException e) { Log.e(TAG, "failed to overwrite the file: " + storage.getUri().toString(), e); - //showErrorActivity(e); showFailedDialog(R.string.overwrite_failed); return; } @@ -748,7 +790,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck if (secondaryStreamUrl == null) { urls = new String[]{selectedStream.getUrl()}; } else { - urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl}; + urls = new String[]{secondaryStreamUrl, selectedStream.getUrl()}; } DownloadManagerService.startMission(context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength); diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java index 3737d1c17..5d4ccf3f8 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java @@ -14,18 +14,23 @@ import android.support.v7.preference.Preference; import android.util.Log; import android.widget.Toast; +import com.nononsenseapps.filepicker.Utils; + import org.schabi.newpipe.R; import org.schabi.newpipe.util.FilePickerActivityHelper; import java.io.File; import java.io.IOException; -import java.net.URI; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; 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_OLD_PATH = true; private String DOWNLOAD_PATH_VIDEO_PREFERENCE; private String DOWNLOAD_PATH_AUDIO_PREFERENCE; @@ -35,41 +40,46 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { private Preference prefPathVideo; private Preference prefPathAudio; - + private Context ctx; + private boolean lastAPIJavaIO; + @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - initKeys(); - updatePreferencesSummary(); - } - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - addPreferencesFromResource(R.xml.download_settings); + DOWNLOAD_PATH_VIDEO_PREFERENCE = getString(R.string.download_path_video_key); + DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key); + DOWNLOAD_STORAGE_API = getString(R.string.downloads_storage_api); + DOWNLOAD_STORAGE_API_DEFAULT = getString(R.string.downloads_storage_api_default); prefPathVideo = findPreference(DOWNLOAD_PATH_VIDEO_PREFERENCE); prefPathAudio = findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE); - updatePathPickers(usingJavaIO()); + lastAPIJavaIO = usingJavaIO(); + + updatePreferencesSummary(); + updatePathPickers(lastAPIJavaIO); findPreference(DOWNLOAD_STORAGE_API).setOnPreferenceChangeListener((preference, value) -> { boolean javaIO = DOWNLOAD_STORAGE_API_DEFAULT.equals(value); - if (!javaIO && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (javaIO == lastAPIJavaIO) return true; + lastAPIJavaIO = javaIO; + + boolean res; + + if (javaIO && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // forget save paths (if necessary) + res = forgetPath(DOWNLOAD_PATH_VIDEO_PREFERENCE); + res |= forgetPath(DOWNLOAD_PATH_AUDIO_PREFERENCE); + } else { + res = hasInvalidPath(DOWNLOAD_PATH_VIDEO_PREFERENCE) || hasInvalidPath(DOWNLOAD_PATH_AUDIO_PREFERENCE); + } + + if (res) { Toast.makeText(ctx, R.string.download_pick_path, Toast.LENGTH_LONG).show(); - - // forget save paths - forgetSAFTree(DOWNLOAD_PATH_VIDEO_PREFERENCE); - forgetSAFTree(DOWNLOAD_PATH_AUDIO_PREFERENCE); - - defaultPreferences.edit() - .putString(DOWNLOAD_PATH_VIDEO_PREFERENCE, "") - .putString(DOWNLOAD_PATH_AUDIO_PREFERENCE, "") - .apply(); - updatePreferencesSummary(); } @@ -78,6 +88,30 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { }); } + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + private boolean forgetPath(String prefKey) { + String path = defaultPreferences.getString(prefKey, ""); + if (path == null || path.isEmpty()) return true; + + if (path.startsWith("file://")) return false; + + // forget SAF path (file:// is compatible with the SAF wrapper) + forgetSAFTree(getContext(), prefKey); + defaultPreferences.edit().putString(prefKey, "").apply(); + + return true; + } + + private boolean hasInvalidPath(String prefKey) { + String value = defaultPreferences.getString(prefKey, null); + return value == null || value.isEmpty(); + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.download_settings); + } + @Override public void onAttach(Context context) { super.onAttach(context); @@ -91,20 +125,25 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { findPreference(DOWNLOAD_STORAGE_API).setOnPreferenceChangeListener(null); } - private void 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_API = getString(R.string.downloads_storage_api); - DOWNLOAD_STORAGE_API_DEFAULT = getString(R.string.downloads_storage_api_default); + private void updatePreferencesSummary() { + showPathInSummary(DOWNLOAD_PATH_VIDEO_PREFERENCE, R.string.download_path_summary, prefPathVideo); + showPathInSummary(DOWNLOAD_PATH_AUDIO_PREFERENCE, R.string.download_path_audio_summary, prefPathAudio); } - private void updatePreferencesSummary() { - prefPathVideo.setSummary( - defaultPreferences.getString(DOWNLOAD_PATH_VIDEO_PREFERENCE, getString(R.string.download_path_summary)) - ); - prefPathAudio.setSummary( - defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, getString(R.string.download_path_audio_summary)) - ); + private void showPathInSummary(String prefKey, @StringRes int defaultString, Preference target) { + String rawUri = defaultPreferences.getString(prefKey, null); + if (rawUri == null || rawUri.isEmpty()) { + target.setSummary(getString(defaultString)); + return; + } + + try { + rawUri = URLDecoder.decode(rawUri, StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + // nothing to do + } + + target.setSummary(rawUri); } private void updatePathPickers(boolean useJavaIO) { @@ -119,20 +158,25 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { ); } + // FIXME: after releasing the old path, all downloads created on the folder becomes inaccessible @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - private void forgetSAFTree(String prefKey) { + private void forgetSAFTree(Context ctx, String prefKey) { + if (IGNORE_RELEASE_OLD_PATH) { + return; + } String oldPath = defaultPreferences.getString(prefKey, ""); - if (oldPath != null && !oldPath.isEmpty() && oldPath.charAt(0) != File.separatorChar) { + if (oldPath != null && !oldPath.isEmpty() && oldPath.charAt(0) != File.separatorChar && !oldPath.startsWith("file://")) { try { - StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(ctx, Uri.parse(oldPath), null); - if (!mainStorage.isDirect()) { - mainStorage.revokePermissions(); - Log.i(TAG, "revokePermissions() [uri=" + oldPath + "] ¡success!"); - } - } catch (IOException err) { - Log.e(TAG, "Error revoking Tree uri permissions [uri=" + oldPath + "]", err); + 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); } } } @@ -167,7 +211,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { if (safPick && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) .putExtra("android.content.extra.SHOW_ADVANCED", true) - .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS); + .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS); } else { i = new Intent(getActivity(), FilePickerActivityHelper.class) .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) @@ -208,27 +252,37 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { if (!usingJavaIO() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // steps: - // 1. acquire permissions on the new save path - // 2. save the new path, if step(1) was successful + // 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, key); try { + ctx.grantUriPermission(ctx.getPackageName(), uri, StoredDirectoryHelper.PERMISSION_FLAGS); + StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(ctx, uri, null); - mainStorage.acquirePermissions(); - Log.i(TAG, "acquirePermissions() [uri=" + uri.toString() + "] ¡success!"); + 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 permissions on " + uri.toString()); + Log.e(TAG, "Error acquiring tree from " + uri.toString(), err); showMessageDialog(R.string.general_error, R.string.no_available_dir); return; } - - defaultPreferences.edit().putString(key, uri.toString()).apply(); } else { - defaultPreferences.edit().putString(key, uri.toString()).apply(); - updatePreferencesSummary(); - - File target = new File(URI.create(uri.toString())); - if (!target.canWrite()) + 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(); } } diff --git a/app/src/main/java/org/schabi/newpipe/streams/DataReader.java b/app/src/main/java/org/schabi/newpipe/streams/DataReader.java index 567fa5229..0e62810c5 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/DataReader.java +++ b/app/src/main/java/org/schabi/newpipe/streams/DataReader.java @@ -16,6 +16,8 @@ public class DataReader { public final static int INTEGER_SIZE = 4; public final static int FLOAT_SIZE = 4; + private final static int BUFFER_SIZE = 128 * 1024;// 128 KiB + private long position = 0; private final SharpStream stream; @@ -229,7 +231,7 @@ public class DataReader { } } - private final byte[] readBuffer = new byte[8 * 1024]; + private final byte[] readBuffer = new byte[BUFFER_SIZE]; private int readOffset; private int readCount; diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java index 61f793e5d..03aab447c 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java @@ -12,7 +12,6 @@ import java.io.IOException; import java.nio.ByteBuffer; /** - * * @author kapodamy */ public class Mp4FromDashWriter { @@ -262,12 +261,12 @@ public class Mp4FromDashWriter { final int ftyp_size = make_ftyp(); // reserve moov space in the output stream - if (outStream.canSetLength()) { + /*if (outStream.canSetLength()) { long length = writeOffset + auxSize; outStream.setLength(length); outSeek(length); - } else { - // hard way + } else {*/ + if (auxSize > 0) { int length = auxSize; byte[] buffer = new byte[8 * 1024];// 8 KiB while (length > 0) { @@ -276,6 +275,7 @@ public class Mp4FromDashWriter { length -= count; } } + if (auxBuffer == null) { outSeek(ftyp_size); } diff --git a/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java b/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java index b874a9eca..37d94cd16 100644 --- a/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java @@ -10,6 +10,9 @@ import java.util.regex.Pattern; 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. * @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) { 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)); - Pattern pattern = Pattern.compile(value); + + final String charset_ld = context.getString(R.string.charset_letters_and_digits_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), "_"); + 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); } diff --git a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java index 1e05983d8..f6b6b459a 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java @@ -28,7 +28,7 @@ public class DownloadInitializer extends Thread { @Override public void run() { - if (mMission.current > 0) mMission.resetState(); + if (mMission.current > 0) mMission.resetState(false,true, DownloadMission.ERROR_NOTHING); int retryCount = 0; while (true) { diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMission.java b/app/src/main/java/us/shandian/giga/get/DownloadMission.java index 9ec3418b0..838acc162 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -2,7 +2,6 @@ package us.shandian.giga.get; import android.os.Handler; import android.os.Message; -import android.support.annotation.NonNull; import android.util.Log; import java.io.File; @@ -86,7 +85,7 @@ public class DownloadMission extends Mission { /** * the post-processing algorithm instance */ - public transient Postprocessing psAlgorithm; + public Postprocessing psAlgorithm; /** * The current resource to download, see {@code urls[current]} and {@code offsets[current]} @@ -483,7 +482,7 @@ public class DownloadMission extends Mission { if (init != null && Thread.currentThread() != init && init.isAlive()) { init.interrupt(); synchronized (blockState) { - resetState(); + resetState(false, true, ERROR_NOTHING); } return; } @@ -525,10 +524,18 @@ public class DownloadMission extends Mission { 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; blocks = -1; - errCode = ERROR_NOTHING; + errCode = errorCode; + errObject = null; fallback = false; unknownLength = false; finishCount = 0; @@ -537,7 +544,10 @@ public class DownloadMission extends Mission { blockState.clear(); threads = new Thread[0]; - Utility.writeToFile(metadata, DownloadMission.this); + if (rollback) current = 0; + + if (persistChanges) + Utility.writeToFile(metadata, DownloadMission.this); } private void initializer() { @@ -633,33 +643,22 @@ public class DownloadMission extends Mission { threads[0].interrupt(); } - /** - * changes the StoredFileHelper for another and saves the changes to the metadata file - * - * @param newStorage the new StoredFileHelper instance to use - */ - public void changeStorage(@NonNull StoredFileHelper newStorage) { - storage = newStorage; - // commit changes on the metadata file - runAsync(-2, this::writeThisToFile); - } - /** * Indicates whatever the backed storage is invalid * * @return {@code true}, if storage is invalid and cannot be used */ public boolean hasInvalidStorage() { - return errCode == ERROR_PROGRESS_LOST || storage == null || storage.isInvalid(); + 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 is "sane", otherwise, {@code false} + * @return {@code true} is this mission its "healthy", otherwise, {@code false} */ - public boolean canDownload() { - return !(isPsFailed() || errCode == ERROR_POSTPROCESSING_HOLD) && !isFinished() && !hasInvalidStorage(); + public boolean isCorrupt() { + return (isPsFailed() || errCode == ERROR_POSTPROCESSING_HOLD) || isFinished() || hasInvalidStorage(); } private boolean doPostprocessing() { diff --git a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java index ced579b20..7a68cd778 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java @@ -137,6 +137,10 @@ public class DownloadRunnable extends Thread { mMission.setThreadBytePosition(mId, total);// download paused, save progress for this block } catch (Exception e) { + if (DEBUG) { + Log.d(TAG, mId + ": position=" + blockPosition + " total=" + total + " stopped due exception", e); + } + mMission.setThreadBytePosition(mId, total); if (!mMission.running || e instanceof ClosedByInterruptException) break; @@ -146,10 +150,6 @@ public class DownloadRunnable extends Thread { break; } - if (DEBUG) { - Log.d(TAG, mId + ":position " + blockPosition + " retrying due exception", e); - } - retry = true; } } diff --git a/app/src/main/java/us/shandian/giga/get/FinishedMission.java b/app/src/main/java/us/shandian/giga/get/FinishedMission.java index 5540b44a1..2a01896fe 100644 --- a/app/src/main/java/us/shandian/giga/get/FinishedMission.java +++ b/app/src/main/java/us/shandian/giga/get/FinishedMission.java @@ -12,5 +12,7 @@ public class FinishedMission extends Mission { length = mission.length;// ¿or mission.done? timestamp = mission.timestamp; kind = mission.kind; + storage = mission.storage; + } } diff --git a/app/src/main/java/us/shandian/giga/get/Mission.java b/app/src/main/java/us/shandian/giga/get/Mission.java index ce201d960..a9ed08fc2 100644 --- a/app/src/main/java/us/shandian/giga/get/Mission.java +++ b/app/src/main/java/us/shandian/giga/get/Mission.java @@ -1,6 +1,5 @@ package us.shandian.giga.get; -import android.net.Uri; import android.support.annotation.NonNull; import java.io.Serializable; @@ -36,15 +35,6 @@ public abstract class Mission implements Serializable { */ public StoredFileHelper storage; - /** - * get the target file on the storage - * - * @return File object - */ - public Uri getDownloadedFileUri() { - return storage.getUri(); - } - /** * Delete the downloaded file * @@ -52,7 +42,7 @@ public abstract class Mission implements Serializable { */ public boolean delete() { if (storage != null) return storage.delete(); - return true; + return true; } /** @@ -65,6 +55,6 @@ public abstract class Mission implements Serializable { public String toString() { Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(timestamp); - return "[" + calendar.getTime().toString() + "] " + getDownloadedFileUri().getPath(); + return "[" + calendar.getTime().toString() + "] " + (storage.isInvalid() ? storage.getName() : storage.getUri()); } } diff --git a/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java b/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java index 6d63b9ff7..4650f75d0 100644 --- a/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java +++ b/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java @@ -35,7 +35,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper { /** * The table name of download missions */ - private static final String FINISHED_MISSIONS_TABLE_NAME = "finished_missions"; + private static final String FINISHED_TABLE_NAME = "finished_missions"; /** * The key to the urls of a mission @@ -58,7 +58,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper { * The statement to create the table */ private static final String MISSIONS_CREATE_TABLE = - "CREATE TABLE " + FINISHED_MISSIONS_TABLE_NAME + " (" + + "CREATE TABLE " + FINISHED_TABLE_NAME + " (" + KEY_PATH + " TEXT NOT NULL, " + KEY_SOURCE + " TEXT NOT NULL, " + KEY_DONE + " INTEGER NOT NULL, " + @@ -111,7 +111,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper { ) ).toString()); - db.insert(FINISHED_MISSIONS_TABLE_NAME, null, values); + db.insert(FINISHED_TABLE_NAME, null, values); } db.setTransactionSuccessful(); db.endTransaction(); @@ -154,10 +154,10 @@ public class FinishedMissionStore extends SQLiteOpenHelper { mission.kind = kind.charAt(0); try { - mission.storage = new StoredFileHelper(context, Uri.parse(path), ""); + 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(path, "", ""); + mission.storage = new StoredFileHelper(null, path, "", ""); } return mission; @@ -170,7 +170,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper { public ArrayList loadFinishedMissions() { SQLiteDatabase database = getReadableDatabase(); - Cursor cursor = database.query(FINISHED_MISSIONS_TABLE_NAME, null, null, + Cursor cursor = database.query(FINISHED_TABLE_NAME, null, null, null, null, null, KEY_TIMESTAMP + " DESC"); int count = cursor.getCount(); @@ -188,33 +188,47 @@ public class FinishedMissionStore extends SQLiteOpenHelper { if (downloadMission == null) throw new NullPointerException("downloadMission is null"); SQLiteDatabase database = getWritableDatabase(); ContentValues values = getValuesOfMission(downloadMission); - database.insert(FINISHED_MISSIONS_TABLE_NAME, null, values); + database.insert(FINISHED_TABLE_NAME, null, values); } public void deleteMission(Mission mission) { if (mission == null) throw new NullPointerException("mission is null"); - String path = mission.getDownloadedFileUri().toString(); + String ts = String.valueOf(mission.timestamp); SQLiteDatabase database = getWritableDatabase(); - if (mission instanceof FinishedMission) - database.delete(FINISHED_MISSIONS_TABLE_NAME, KEY_TIMESTAMP + " = ?, " + KEY_PATH + " = ?", new String[]{path}); - else + 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 path = mission.getDownloadedFileUri().toString(); + String ts = String.valueOf(mission.timestamp); int rowsAffected; - if (mission instanceof FinishedMission) - rowsAffected = database.update(FINISHED_MISSIONS_TABLE_NAME, values, KEY_PATH + " = ?", new String[]{path}); - else + 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); diff --git a/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java b/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java index 650725a76..327b9149e 100644 --- a/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java +++ b/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java @@ -12,7 +12,7 @@ public class CircularFileWriter extends SharpStream { private final static int QUEUE_BUFFER_SIZE = 8 * 1024;// 8 KiB private final static int NOTIFY_BYTES_INTERVAL = 64 * 1024;// 64 KiB - private final static int THRESHOLD_AUX_LENGTH = 3 * 1024 * 1024;// 3 MiB + private final static int THRESHOLD_AUX_LENGTH = 15 * 1024 * 1024;// 15 MiB private OffsetChecker callback; @@ -44,41 +44,84 @@ public class CircularFileWriter extends SharpStream { reportPosition = NOTIFY_BYTES_INTERVAL; } - private void flushAuxiliar() throws IOException { + private void flushAuxiliar(long amount) throws IOException { if (aux.length < 1) { return; } - boolean underflow = out.getOffset() >= out.length; - out.flush(); aux.flush(); + boolean underflow = aux.offset < aux.length || out.offset < out.length; + aux.target.seek(0); out.target.seek(out.length); - long length = aux.length; - out.length += aux.length; - + long length = amount; while (length > 0) { int read = (int) Math.min(length, Integer.MAX_VALUE); read = aux.target.read(aux.queue, 0, Math.min(read, aux.queue.length)); + if (read < 1) { + amount -= length; + break; + } + out.writeProof(aux.queue, read); length -= read; } if (underflow) { - out.offset += aux.offset; - out.target.seek(out.offset); + 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 = out.length; + 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; + byte[] buffer = new byte[128 * 1024]; // 128 KiB + + 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); } @@ -94,7 +137,7 @@ public class CircularFileWriter extends SharpStream { * @throws IOException if an I/O error occurs */ public long finalizeFile() throws IOException { - flushAuxiliar(); + flushAuxiliar(aux.length); out.flush(); @@ -148,7 +191,7 @@ public class CircularFileWriter extends SharpStream { if (end == -1) { available = Integer.MAX_VALUE; } else if (end < offsetOut) { - throw new IOException("The reported offset is invalid: " + String.valueOf(offsetOut)); + throw new IOException("The reported offset is invalid: " + end + "<" + offsetOut); } else { available = end - offsetOut; } @@ -167,16 +210,10 @@ public class CircularFileWriter extends SharpStream { length = aux.length + len; } - if (length > available || length < THRESHOLD_AUX_LENGTH) { - aux.write(b, off, len); - } else { - if (underflow) { - aux.write(b, off, len); - flushAuxiliar(); - } else { - flushAuxiliar(); - out.write(b, off, len);// write directly on the output - } + aux.write(b, off, len); + + if (length >= THRESHOLD_AUX_LENGTH && length <= available) { + flushAuxiliar(available); } } else { if (underflow) { @@ -234,8 +271,13 @@ public class CircularFileWriter extends SharpStream { @Override public void seek(long offset) throws IOException { long total = out.length + aux.length; + if (offset == total) { - return;// nothing to do + // 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 @@ -409,6 +451,9 @@ public class CircularFileWriter extends SharpStream { } protected void seek(long absoluteOffset) throws IOException { + if (absoluteOffset == offset) { + return;// nothing to do + } offset = absoluteOffset; target.seek(absoluteOffset); } diff --git a/app/src/main/java/us/shandian/giga/io/FileStreamSAF.java b/app/src/main/java/us/shandian/giga/io/FileStreamSAF.java index cb4786280..ec6629268 100644 --- a/app/src/main/java/us/shandian/giga/io/FileStreamSAF.java +++ b/app/src/main/java/us/shandian/giga/io/FileStreamSAF.java @@ -137,4 +137,9 @@ public class FileStreamSAF extends SharpStream { public void seek(long offset) throws IOException { channel.position(offset); } + + @Override + public long length() throws IOException { + return channel.size(); + } } diff --git a/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java b/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java index f5c2fd3f5..eb3c9b817 100644 --- a/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java +++ b/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java @@ -4,8 +4,10 @@ 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.annotation.RequiresApi; @@ -13,8 +15,13 @@ 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.Arrays; +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; @@ -22,14 +29,27 @@ public class StoredDirectoryHelper { private File ioTree; private DocumentFile docTree; - private ContentResolver contentResolver; + private Context context; private String tag; @RequiresApi(Build.VERSION_CODES.LOLLIPOP) public StoredDirectoryHelper(@NonNull Context context, @NonNull Uri path, String tag) throws IOException { - this.contentResolver = context.getContentResolver(); this.tag = tag; + + 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); + } + this.docTree = DocumentFile.fromTreeUri(context, path); if (this.docTree == null) @@ -37,23 +57,75 @@ public class StoredDirectoryHelper { } @TargetApi(Build.VERSION_CODES.KITKAT) - public StoredDirectoryHelper(@NonNull String location, String tag) { + public StoredDirectoryHelper(@NonNull URI location, String tag) { ioTree = new File(location); this.tag = tag; } - @Nullable public StoredFileHelper createFile(String filename, String mime) { + return createFile(filename, mime, false); + } + + public StoredFileHelper createUniqueFile(String name, String mime) { + ArrayList 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, tag); - storage.sourceTree = Uri.fromFile(ioTree).toString(); - } else { - storage = new StoredFileHelper(docTree, contentResolver, filename, mime, tag); - storage.sourceTree = docTree.getUri().toString(); - } + if (docTree == null) + storage = new StoredFileHelper(ioTree, filename, mime); + else + storage = new StoredFileHelper(context, docTree, filename, mime, safe); } catch (IOException e) { return null; } @@ -63,67 +135,6 @@ public class StoredDirectoryHelper { return storage; } - public StoredFileHelper createUniqueFile(String filename, String mime) { - ArrayList existingNames = new ArrayList<>(50); - - String ext; - - int dotIndex = filename.lastIndexOf('.'); - if (dotIndex < 0 || (dotIndex == filename.length() - 1)) { - ext = ""; - } else { - ext = filename.substring(dotIndex); - filename = filename.substring(0, dotIndex - 1); - } - - String name; - if (docTree == null) { - for (File file : ioTree.listFiles()) { - name = file.getName().toLowerCase(); - if (name.startsWith(filename)) existingNames.add(name); - } - } else { - for (DocumentFile file : docTree.listFiles()) { - name = file.getName(); - if (name == null) continue; - name = name.toLowerCase(); - if (name.startsWith(filename)) existingNames.add(name); - } - } - - boolean free = true; - String lwFilename = filename.toLowerCase(); - for (String testName : existingNames) { - if (testName.equals(lwFilename)) { - free = false; - break; - } - } - - if (free) return createFile(filename, mime); - - String[] sortedNames = existingNames.toArray(new String[0]); - Arrays.sort(sortedNames); - - String newName; - int downloadIndex = 0; - do { - newName = filename + " (" + downloadIndex + ")" + ext; - ++downloadIndex; - if (downloadIndex == 1000) { // Probably an error on our side - newName = System.currentTimeMillis() + ext; - break; - } - } while (Arrays.binarySearch(sortedNames, newName) >= 0); - - - return createFile(newName, mime); - } - - public boolean isDirect() { - return docTree == null; - } - public Uri getUri() { return docTree == null ? Uri.fromFile(ioTree) : docTree.getUri(); } @@ -136,34 +147,18 @@ public class StoredDirectoryHelper { return tag; } - public void acquirePermissions() throws IOException { - if (docTree == null) return; - - try { - contentResolver.takePersistableUriPermission(docTree.getUri(), PERMISSION_FLAGS); - } catch (Throwable e) { - throw new IOException(e); - } - } - - public void revokePermissions() throws IOException { - if (docTree == null) return; - - try { - contentResolver.releasePersistableUriPermission(docTree.getUri(), PERMISSION_FLAGS); - } catch (Throwable e) { - throw new IOException(e); - } - } - public Uri findFile(String filename) { - if (docTree == null) - return Uri.fromFile(new File(ioTree, filename)); + if (docTree == null) { + File res = new File(ioTree, filename); + return res.exists() ? Uri.fromFile(res) : null; + } - // findFile() method is very slow - DocumentFile file = docTree.findFile(filename); + DocumentFile res = findFileSAFHelper(context, docTree, filename); + return res == null ? null : res.getUri(); + } - return file == null ? null : file.getUri(); + public boolean canWrite() { + return docTree == null ? ioTree.canWrite() : docTree.canWrite(); } @NonNull @@ -172,4 +167,76 @@ public class StoredDirectoryHelper { return docTree == null ? Uri.fromFile(ioTree).toString() : docTree.getUri().toString(); } + + //////////////////// + // Utils + /////////////////// + + private static void addIfStartWith(ArrayList 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; + } + } diff --git a/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java b/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java index 0db442f1c..f90a756a9 100644 --- a/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java +++ b/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java @@ -8,6 +8,7 @@ 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; @@ -25,79 +26,52 @@ public class StoredFileHelper implements Serializable { private transient DocumentFile docFile; private transient DocumentFile docTree; private transient File ioFile; - private transient ContentResolver contentResolver; + private transient Context context; protected String source; - String sourceTree; + private String sourceTree; protected String tag; private String srcName; private String srcType; - public StoredFileHelper(String filename, String mime, String tag) { + 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(DocumentFile tree, ContentResolver contentResolver, String filename, String mime, String tag) throws IOException { + StoredFileHelper(@Nullable Context context, DocumentFile tree, String filename, String mime, boolean safe) throws IOException { this.docTree = tree; - this.contentResolver = contentResolver; + this.context = context; - // this is very slow, because SAF does not allow overwrite - DocumentFile res = this.docTree.findFile(filename); + DocumentFile res; - if (res != null && res.exists() && res.isDirectory()) { - if (!res.delete()) - throw new IOException("Directory with the same name found but cannot delete"); - res = null; - } - - if (res == null) { - res = this.docTree.createFile(mime == null ? DEFAULT_MIME : mime, filename); + if (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 = res.getUri().toString(); - this.srcName = getName(); - this.srcType = getType(); + + this.source = docFile.getUri().toString(); + this.sourceTree = docTree.getUri().toString(); + + this.srcName = this.docFile.getName(); + this.srcType = this.docFile.getType(); } - @TargetApi(Build.VERSION_CODES.KITKAT) - public StoredFileHelper(Context context, @NonNull Uri path, String tag) throws IOException { - this.source = path.toString(); - this.tag = tag; - - if (path.getScheme() == null || path.getScheme().equalsIgnoreCase("file")) { - this.ioFile = new File(URI.create(this.source)); - } else { - DocumentFile file = DocumentFile.fromSingleUri(context, path); - if (file == null) - throw new UnsupportedOperationException("Cannot get the file via SAF"); - - this.contentResolver = context.getContentResolver(); - this.docFile = file; - - try { - this.contentResolver.takePersistableUriPermission(docFile.getUri(), StoredDirectoryHelper.PERMISSION_FLAGS); - } catch (Exception e) { - throw new IOException(e); - } - } - - this.srcName = getName(); - this.srcType = getType(); - } - - public StoredFileHelper(File location, String filename, String tag) throws IOException { + StoredFileHelper(File location, String filename, String mime) throws IOException { this.ioFile = new File(location, filename); - this.tag = tag; if (this.ioFile.exists()) { if (!this.ioFile.isFile() && !this.ioFile.delete()) @@ -108,22 +82,58 @@ public class StoredFileHelper implements Serializable { } 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(storage.srcName, storage.srcType, storage.tag); + return new StoredFileHelper(treeUri, storage.srcName, storage.srcType, storage.tag); - StoredFileHelper instance = new StoredFileHelper(context, Uri.parse(storage.source), storage.tag); + StoredFileHelper instance = new StoredFileHelper(context, treeUri, Uri.parse(storage.source), storage.tag); - if (storage.sourceTree != null) { - instance.docTree = DocumentFile.fromTreeUri(context, Uri.parse(instance.sourceTree)); - - if (instance.docTree == null) - throw new IOException("Cannot deserialize the tree, ¿revoked permissions?"); - } + // 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; } @@ -143,13 +153,14 @@ public class StoredFileHelper implements Serializable { who.startActivityForResult(intent, requestCode); } + public SharpStream getStream() throws IOException { invalid(); if (docFile == null) return new FileStream(ioFile); else - return new FileStreamSAF(contentResolver, docFile.getUri()); + return new FileStreamSAF(context.getContentResolver(), docFile.getUri()); } /** @@ -173,6 +184,12 @@ public class StoredFileHelper implements Serializable { 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(); @@ -182,17 +199,17 @@ public class StoredFileHelper implements Serializable { } public boolean delete() { - invalid(); - + 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; - contentResolver.releasePersistableUriPermission(docFile.getUri(), flags); + context.getContentResolver().releasePersistableUriPermission(docFile.getUri(), flags); } catch (Exception ex) { - // ¿what happen? + // nothing to do } return res; @@ -209,18 +226,22 @@ public class StoredFileHelper implements Serializable { return docFile == null ? ioFile.canWrite() : docFile.canWrite(); } - public File getIOFile() { - return ioFile; - } - public String getName() { - if (source == null) return srcName; - return docFile == null ? ioFile.getName() : docFile.getName(); + 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) return srcType; - return docFile == null ? DEFAULT_MIME : docFile.getType();// not obligatory for Java IO + if (source == null || docFile == null) + return srcType; + + String type = docFile.getType(); + return type == null ? srcType : type; } public String getTag() { @@ -231,29 +252,41 @@ public class StoredFileHelper implements Serializable { if (source == null) return false; boolean exists = docFile == null ? ioFile.exists() : docFile.exists(); - boolean asFile = docFile == null ? ioFile.isFile() : docFile.isFile();// ¿docFile.isVirtual() means is no-physical? + boolean isFile = docFile == null ? ioFile.isFile() : docFile.isFile();// ¿docFile.isVirtual() means is no-physical? - return exists && asFile; + return exists && isFile; } public boolean create() { invalid(); + boolean result; if (docFile == null) { try { - return ioFile.createNewFile(); + 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 (docTree == null || docFile.getName() == null) return false; + if (result) { + source = (docFile == null ? Uri.fromFile(ioFile) : docFile.getUri()).toString(); + srcName = getName(); + srcType = getType(); + } - DocumentFile res = docTree.createFile(docFile.getName(), docFile.getType() == null ? DEFAULT_MIME : docFile.getType()); - if (res == null) return false; - - docFile = res; - return true; + return result; } public void invalidate() { @@ -264,20 +297,25 @@ public class StoredFileHelper implements Serializable { source = null; - sourceTree = null; docTree = null; docFile = null; ioFile = null; - contentResolver = null; - } - - private void invalid() { - if (source == null) - throw new IllegalStateException("In invalid state"); + context = null; } public boolean equals(StoredFileHelper storage) { - if (this.isInvalid() != storage.isInvalid()) return false; + 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()) @@ -298,4 +336,46 @@ public class StoredFileHelper implements Serializable { 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); + } } diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java index 98ab29dbb..f12c1c2d2 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java @@ -11,7 +11,7 @@ import java.io.IOException; class Mp4FromDashMuxer extends Postprocessing { Mp4FromDashMuxer() { - super(2 * 1024 * 1024/* 2 MiB */, true); + super(3 * 1024 * 1024/* 3 MiB */, true); } @Override diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java index 7bc32ea05..3d10628e7 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java @@ -8,6 +8,7 @@ import org.schabi.newpipe.streams.io.SharpStream; import java.io.File; import java.io.IOException; +import java.io.Serializable; import us.shandian.giga.get.DownloadMission; import us.shandian.giga.io.ChunkFileInputStream; @@ -19,7 +20,7 @@ import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING; import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD; import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; -public abstract class Postprocessing { +public abstract class Postprocessing implements Serializable { static transient final byte OK_RESULT = ERROR_NOTHING; @@ -28,12 +29,10 @@ public abstract class Postprocessing { public transient static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4"; public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a"; - public static Postprocessing getAlgorithm(String algorithmName, String[] args) { + public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args, @NonNull File cacheDir) { Postprocessing instance; - if (null == algorithmName) { - throw new NullPointerException("algorithmName"); - } else switch (algorithmName) { + switch (algorithmName) { case ALGORITHM_TTML_CONVERTER: instance = new TtmlConverter(); break; @@ -47,13 +46,14 @@ public abstract class Postprocessing { instance = new M4aNoDash(); break; /*case "example-algorithm": - instance = new ExampleAlgorithm(mission);*/ + instance = new ExampleAlgorithm();*/ default: throw new RuntimeException("Unimplemented post-processing algorithm: " + algorithmName); } instance.args = args; - instance.name = algorithmName; + instance.name = algorithmName;// for debug only, maybe remove this field in the future + instance.cacheDir = cacheDir; return instance; } @@ -125,7 +125,6 @@ public abstract class Postprocessing { return -1; }; - // TODO: use Context.getCache() for this operation temp = new File(cacheDir, mission.storage.getName() + ".tmp"); out = new CircularFileWriter(mission.storage.getStream(), temp, checker); diff --git a/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java index 37295f2e3..3d5ecb3cd 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java @@ -13,7 +13,7 @@ import java.io.IOException; class WebMMuxer extends Postprocessing { WebMMuxer() { - super(2048 * 1024/* 2 MiB */, true); + super(5 * 1024 * 1024/* 5 MiB */, true); } @Override diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java index 3624fb6c2..479c4b92f 100644 --- a/app/src/main/java/us/shandian/giga/service/DownloadManager.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java @@ -62,13 +62,15 @@ public class DownloadManager { * @param context Context for the data source for finished downloads * @param handler Thread required for Messaging */ - DownloadManager(@NonNull Context context, Handler handler) { + DownloadManager(@NonNull Context context, Handler handler, StoredDirectoryHelper storageVideo, StoredDirectoryHelper storageAudio) { if (DEBUG) { Log.d(TAG, "new DownloadManager instance. 0x" + Integer.toHexString(this.hashCode())); } mFinishedMissionStore = new FinishedMissionStore(context); mHandler = handler; + mMainStorageAudio = storageAudio; + mMainStorageVideo = storageVideo; mMissionsFinished = loadFinishedMissions(); mPendingMissionsDir = getPendingDir(context); @@ -129,91 +131,59 @@ public class DownloadManager { } for (File sub : subs) { - if (sub.isFile()) { - DownloadMission mis = Utility.readFromFile(sub); + if (!sub.isFile()) continue; - if (mis == null) { - //noinspection ResultOfMethodCallIgnored - sub.delete(); - } else { - if (mis.isFinished()) { - //noinspection ResultOfMethodCallIgnored - sub.delete(); - continue; - } - - boolean exists; - try { - mis.storage = StoredFileHelper.deserialize(mis.storage, ctx); - exists = !mis.storage.isInvalid() && mis.storage.existsAsFile(); - - } catch (Exception ex) { - Log.e(TAG, "Failed to load the file source of " + mis.storage.toString()); - mis.storage.invalidate(); - exists = false; - } - - if (mis.isPsRunning()) { - if (mis.psAlgorithm.worksOnSameFile) { - // Incomplete post-processing results in a corrupted download file - // because the selected algorithm works on the same file to save space. - if (exists && !mis.storage.delete()) - Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath()); - - exists = true; - } - - mis.psState = 0; - mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED; - mis.errObject = null; - } else if (!exists) { - - StoredDirectoryHelper mainStorage = getMainStorage(mis.storage.getTag()); - - if (!mis.storage.isInvalid() && !mis.storage.create()) { - // using javaIO cannot recreate the file - // using SAF in older devices (no tree available) - // - // force the user to pick again the save path - mis.storage.invalidate(); - } else if (mainStorage != null) { - // if the user has changed the save path before this download, the original save path will be lost - StoredFileHelper newStorage = mainStorage.createFile(mis.storage.getName(), mis.storage.getType()); - if (newStorage == null) - mis.storage.invalidate(); - else - mis.storage = newStorage; - } - - if (mis.isInitialized()) { - // the progress is lost, reset mission state - DownloadMission m = new DownloadMission(mis.urls, mis.storage, mis.kind, mis.psAlgorithm); - m.timestamp = mis.timestamp; - m.threadCount = mis.threadCount; - m.source = mis.source; - m.nearLength = mis.nearLength; - m.enqueued = mis.enqueued; - m.errCode = DownloadMission.ERROR_PROGRESS_LOST; - mis = m; - } - } - - if (mis.psAlgorithm != null) mis.psAlgorithm.cacheDir = ctx.getCacheDir(); - - mis.running = false; - mis.recovered = exists; - mis.metadata = sub; - mis.maxRetry = mPrefMaxRetry; - mis.mHandler = mHandler; - - mMissionsPending.add(mis); - } + DownloadMission mis = Utility.readFromFile(sub); + if (mis == null || mis.isFinished()) { + //noinspection ResultOfMethodCallIgnored + sub.delete(); + continue; } + + boolean exists; + try { + mis.storage = StoredFileHelper.deserialize(mis.storage, ctx); + exists = !mis.storage.isInvalid() && mis.storage.existsAsFile(); + } catch (Exception ex) { + Log.e(TAG, "Failed to load the file source of " + mis.storage.toString(), 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. + if (exists && !mis.storage.delete()) + Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath()); + + exists = true; + } + + mis.psState = 0; + mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED; + mis.errObject = null; + } else if (!exists) { + 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.cacheDir = pickAvailableCacheDir(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)); - } } /** @@ -313,6 +283,25 @@ public class DownloadManager { } } + 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 path @@ -392,7 +381,7 @@ public class DownloadManager { synchronized (this) { for (DownloadMission mission : mMissionsPending) { - if (mission.running || !mission.canDownload()) continue; + if (mission.running || mission.isCorrupt()) continue; flag = true; mission.start(); @@ -482,7 +471,7 @@ public class DownloadManager { int paused = 0; synchronized (this) { for (DownloadMission mission : mMissionsPending) { - if (!mission.canDownload() || mission.isPsRunning()) continue; + if (mission.isCorrupt() || mission.isPsRunning()) continue; if (mission.running && isMetered) { paused++; @@ -542,6 +531,20 @@ public class DownloadManager { return MissionState.None; } + private static boolean isDirectoryAvailable(File directory) { + return directory != null && directory.canWrite(); + } + + static File pickAvailableCacheDir(@NonNull Context ctx) { + if (isDirectoryAvailable(ctx.getExternalCacheDir())) + return ctx.getExternalCacheDir(); + else if (isDirectoryAvailable(ctx.getCacheDir())) + return ctx.getCacheDir(); + + // 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; @@ -656,7 +659,7 @@ public class DownloadManager { synchronized (DownloadManager.this) { for (DownloadMission mission : mMissionsPending) { - if (hidden.contains(mission) || mission.canDownload()) + if (hidden.contains(mission) || mission.isCorrupt()) continue; if (mission.running) diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index deed9e8e3..da63cb545 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -6,6 +6,7 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -40,6 +41,7 @@ import org.schabi.newpipe.player.helper.LockManager; import java.io.File; import java.io.IOException; +import java.net.URI; import java.util.ArrayList; import us.shandian.giga.get.DownloadMission; @@ -65,14 +67,15 @@ public class DownloadManagerService extends Service { private static final int DOWNLOADS_NOTIFICATION_ID = 1001; private static final String EXTRA_URLS = "DownloadManagerService.extra.urls"; - private static final String EXTRA_PATH = "DownloadManagerService.extra.path"; private static final String EXTRA_KIND = "DownloadManagerService.extra.kind"; private static final String EXTRA_THREADS = "DownloadManagerService.extra.threads"; private static final String EXTRA_POSTPROCESSING_NAME = "DownloadManagerService.extra.postprocessingName"; private static final String EXTRA_POSTPROCESSING_ARGS = "DownloadManagerService.extra.postprocessingArgs"; private static final String EXTRA_SOURCE = "DownloadManagerService.extra.source"; private static final String EXTRA_NEAR_LENGTH = "DownloadManagerService.extra.nearLength"; - private static final String EXTRA_MAIN_STORAGE_TAG = "DownloadManagerService.extra.tag"; + 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_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished"; @@ -136,7 +139,9 @@ public class DownloadManagerService extends Service { } }; - mManager = new DownloadManager(this, mHandler); + mPrefs = PreferenceManager.getDefaultSharedPreferences(this); + + mManager = new DownloadManager(this, mHandler, getVideoStorage(), getAudioStorage()); Intent openDownloadListIntent = new Intent(this, DownloadActivity.class) .setAction(Intent.ACTION_MAIN); @@ -182,7 +187,6 @@ public class DownloadManagerService extends Service { registerReceiver(mNetworkStateListener, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); } - mPrefs = PreferenceManager.getDefaultSharedPreferences(this); mPrefs.registerOnSharedPreferenceChangeListener(mPrefChangeListener); handlePreferenceChange(mPrefs, getString(R.string.downloads_cross_network)); @@ -190,8 +194,6 @@ public class DownloadManagerService extends Service { handlePreferenceChange(mPrefs, getString(R.string.downloads_queue_limit)); mLock = new LockManager(this); - - setupStorageAPI(true); } @Override @@ -347,11 +349,12 @@ public class DownloadManagerService extends Service { } else if (key.equals(getString(R.string.downloads_queue_limit))) { mManager.mPrefQueueLimit = prefs.getBoolean(key, true); } else if (key.equals(getString(R.string.downloads_storage_api))) { - setupStorageAPI(false); + mManager.mMainStorageVideo = loadMainStorage(getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO); + mManager.mMainStorageAudio = loadMainStorage(getString(R.string.download_path_audio_key), DownloadManager.TAG_AUDIO); } else if (key.equals(getString(R.string.download_path_video_key))) { - loadMainStorage(key, DownloadManager.TAG_VIDEO, false); + mManager.mMainStorageVideo = loadMainStorage(key, DownloadManager.TAG_VIDEO); } else if (key.equals(getString(R.string.download_path_audio_key))) { - loadMainStorage(key, DownloadManager.TAG_AUDIO, false); + mManager.mMainStorageAudio = loadMainStorage(key, DownloadManager.TAG_AUDIO); } } @@ -387,36 +390,46 @@ public class DownloadManagerService extends Service { Intent intent = new Intent(context, DownloadManagerService.class); intent.setAction(Intent.ACTION_RUN); intent.putExtra(EXTRA_URLS, urls); - intent.putExtra(EXTRA_PATH, storage.getUri()); intent.putExtra(EXTRA_KIND, kind); intent.putExtra(EXTRA_THREADS, threads); intent.putExtra(EXTRA_SOURCE, source); intent.putExtra(EXTRA_POSTPROCESSING_NAME, psName); intent.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs); intent.putExtra(EXTRA_NEAR_LENGTH, nearLength); - intent.putExtra(EXTRA_MAIN_STORAGE_TAG, storage.getTag()); + + intent.putExtra(EXTRA_PARENT_PATH, storage.getParentUri()); + intent.putExtra(EXTRA_PATH, storage.getUri()); + intent.putExtra(EXTRA_STORAGE_TAG, storage.getTag()); + context.startService(intent); } - public void startMission(Intent intent) { + private void startMission(Intent intent) { String[] urls = intent.getStringArrayExtra(EXTRA_URLS); Uri path = intent.getParcelableExtra(EXTRA_PATH); + Uri parentPath = intent.getParcelableExtra(EXTRA_PARENT_PATH); int threads = intent.getIntExtra(EXTRA_THREADS, 1); char kind = intent.getCharExtra(EXTRA_KIND, '?'); String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME); String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS); String source = intent.getStringExtra(EXTRA_SOURCE); long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0); - String tag = intent.getStringExtra(EXTRA_MAIN_STORAGE_TAG); + String tag = intent.getStringExtra(EXTRA_STORAGE_TAG); StoredFileHelper storage; try { - storage = new StoredFileHelper(this, path, tag); + storage = new StoredFileHelper(this, parentPath, path, tag); } catch (IOException e) { throw new RuntimeException(e);// this never should happen } - final DownloadMission mission = new DownloadMission(urls, storage, kind, Postprocessing.getAlgorithm(psName, psArgs)); + Postprocessing ps; + if (psName == null) + ps = null; + else + ps = Postprocessing.getAlgorithm(psName, psArgs, DownloadManager.pickAvailableCacheDir(this)); + + final DownloadMission mission = new DownloadMission(urls, storage, kind, ps); mission.threadCount = threads; mission.source = source; mission.nearLength = nearLength; @@ -525,60 +538,63 @@ public class DownloadManagerService extends Service { mLockAcquired = acquire; } - private void setupStorageAPI(boolean acquire) { - loadMainStorage(getString(R.string.download_path_audio_key), DownloadManager.TAG_VIDEO, acquire); - loadMainStorage(getString(R.string.download_path_video_key), DownloadManager.TAG_AUDIO, acquire); + private StoredDirectoryHelper getVideoStorage() { + return loadMainStorage(getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO); } - void loadMainStorage(String prefKey, String tag, boolean acquire) { + private StoredDirectoryHelper getAudioStorage() { + return loadMainStorage(getString(R.string.download_path_audio_key), DownloadManager.TAG_AUDIO); + } + + + private StoredDirectoryHelper loadMainStorage(String prefKey, String tag) { String path = mPrefs.getString(prefKey, null); final String JAVA_IO = getString(R.string.downloads_storage_api_default); boolean useJavaIO = JAVA_IO.equals(mPrefs.getString(getString(R.string.downloads_storage_api), JAVA_IO)); final String defaultPath; - if (tag.equals(DownloadManager.TAG_VIDEO)) - defaultPath = Environment.DIRECTORY_MOVIES; - else// if (tag.equals(DownloadManager.TAG_AUDIO)) - defaultPath = Environment.DIRECTORY_MUSIC; - - StoredDirectoryHelper mainStorage; - if (path == null || path.isEmpty()) { - mainStorage = useJavaIO ? new StoredDirectoryHelper(defaultPath, tag) : null; - } else { - - if (path.charAt(0) == File.separatorChar) { - Log.i(TAG, "Migrating old save path: " + path); - - useJavaIO = true; - path = Uri.fromFile(new File(path)).toString(); - - mPrefs.edit().putString(prefKey, path).apply(); - } - - if (useJavaIO) { - mainStorage = new StoredDirectoryHelper(path, tag); - } else { - - // tree api is not available in older versions - if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - mainStorage = null; - } else { - try { - mainStorage = new StoredDirectoryHelper(this, Uri.parse(path), tag); - if (acquire) mainStorage.acquirePermissions(); - } catch (IOException e) { - Log.e(TAG, "Failed to load the storage of " + tag + " from path: " + path, e); - mainStorage = null; - } - } - } + switch (tag) { + case DownloadManager.TAG_VIDEO: + defaultPath = Environment.DIRECTORY_MOVIES; + break; + case DownloadManager.TAG_AUDIO: + defaultPath = Environment.DIRECTORY_MUSIC; + break; + default: + return null; } - if (tag.equals(DownloadManager.TAG_VIDEO)) - mManager.mMainStorageVideo = mainStorage; - else// if (tag.equals(DownloadManager.TAG_AUDIO)) - mManager.mMainStorageAudio = mainStorage; + if (path == null || path.isEmpty()) { + return useJavaIO ? new StoredDirectoryHelper(new File(defaultPath).toURI(), tag) : null; + } + + if (path.charAt(0) == File.separatorChar) { + Log.i(TAG, "Migrating old save path: " + path); + + useJavaIO = true; + path = Uri.fromFile(new File(path)).toString(); + + mPrefs.edit().putString(prefKey, path).apply(); + } + + boolean override = path.startsWith(ContentResolver.SCHEME_FILE) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; + if (useJavaIO || override) { + return new StoredDirectoryHelper(URI.create(path), tag); + } + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return null;// SAF Directory API is not available in older versions + } + + 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; } //////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index 4d80588e0..1892f4437 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -41,7 +41,9 @@ import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.NavigationHelper; +import java.io.File; import java.lang.ref.WeakReference; +import java.net.URI; import java.util.ArrayList; import java.util.Collections; @@ -346,7 +348,7 @@ public class MissionAdapter extends Adapter { uri = FileProvider.getUriForFile( mContext, BuildConfig.APPLICATION_ID + ".provider", - mission.storage.getIOFile() + new File(URI.create(mission.storage.getUri().toString())) ); } else { uri = mission.storage.getUri(); @@ -384,10 +386,18 @@ public class MissionAdapter extends Adapter { } 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; - String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1)); + mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1)); return mimeType == null ? DEFAULT_MIME_TYPE : mimeType; } @@ -476,6 +486,7 @@ public class MissionAdapter extends Adapter { return; case ERROR_PROGRESS_LOST: msg = R.string.error_progress_lost; + break; default: if (mission.errCode >= 100 && mission.errCode < 600) { msgEx = "HTTP " + mission.errCode; @@ -554,7 +565,12 @@ public class MissionAdapter extends Adapter { return true; case R.id.retry: if (mission.hasInvalidStorage()) { - mRecover.tryRecover(mission); + mDownloadManager.tryRecover(mission); + if (mission.storage.isInvalid()) + mRecover.tryRecover(mission); + else + recoverMission(mission); + return true; } mission.psContinue(true); @@ -672,13 +688,12 @@ public class MissionAdapter extends Adapter { if (mDeleter != null) mDeleter.resume(); } - public void recoverMission(DownloadMission mission, StoredFileHelper newStorage) { + public void recoverMission(DownloadMission mission) { for (ViewHolderItem h : mPendingDownloadsItems) { if (mission != h.item.mission) continue; - mission.changeStorage(newStorage); - mission.errCode = DownloadMission.ERROR_NOTHING; mission.errObject = null; + mission.resetState(true, false, DownloadMission.ERROR_NOTHING); h.status.setText(UNDEFINED_PROGRESS); h.state = -1; @@ -822,9 +837,9 @@ public class MissionAdapter extends Adapter { if (mission != null) { if (mission.hasInvalidStorage()) { - retry.setEnabled(true); - delete.setEnabled(true); - showError.setEnabled(true); + retry.setVisible(true); + delete.setVisible(true); + showError.setVisible(true); } else if (mission.isPsRunning()) { switch (mission.errCode) { case ERROR_INSUFFICIENT_STORAGE: diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index 82ab777b0..bd5ce9215 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -9,6 +9,7 @@ import android.content.SharedPreferences; import android.os.Bundle; import android.os.IBinder; 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.LinearLayoutManager; @@ -66,14 +67,7 @@ public class MissionsFragment extends Fragment { mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty); mAdapter.deleterLoad(getView()); - mAdapter.setRecover(mission -> - StoredFileHelper.requestSafWithFileCreation( - MissionsFragment.this, - REQUEST_DOWNLOAD_PATH_SAF, - mission.storage.getName(), - mission.storage.getType() - ) - ); + mAdapter.setRecover(MissionsFragment.this::recoverMission); setAdapterButtons(); @@ -92,7 +86,7 @@ public class MissionsFragment extends Fragment { }; @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); mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); @@ -239,8 +233,18 @@ public class MissionsFragment extends Fragment { 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 - public void onSaveInstanceState(Bundle outState) { + public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); if (mAdapter != null) { @@ -285,8 +289,9 @@ public class MissionsFragment extends Fragment { } try { - StoredFileHelper storage = new StoredFileHelper(mContext, data.getData(), unsafeMissionTarget.storage.getTag()); - mAdapter.recoverMission(unsafeMissionTarget, storage); + 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(); } diff --git a/app/src/main/java/us/shandian/giga/util/Utility.java b/app/src/main/java/us/shandian/giga/util/Utility.java index d77e598d8..793cbea18 100644 --- a/app/src/main/java/us/shandian/giga/util/Utility.java +++ b/app/src/main/java/us/shandian/giga/util/Utility.java @@ -9,6 +9,7 @@ import android.support.annotation.DrawableRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; +import android.util.Log; import android.widget.Toast; import org.schabi.newpipe.R; @@ -81,6 +82,7 @@ public class Utility { objectInputStream = new ObjectInputStream(new FileInputStream(file)); object = (T) objectInputStream.readObject(); } catch (Exception e) { + Log.e("Utility", "Failed to deserialize the object", e); object = null; } diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index eee110474..4cc394357 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -442,8 +442,8 @@ abrir en modo popup Mostrar error Codigo - No se puede crear la carpeta de destino - No se puede crear el archivo + No se puede crear el archivo + No se puede crear la carpeta de destino Permiso denegado por el sistema Fallo la conexión segura No se pudo encontrar el servidor diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index b2be3135b..42df857c1 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -176,13 +176,17 @@ - file_rename + file_rename_charset file_replacement_character _ + + CHARSET_LETTERS_AND_DIGITS + CHARSET_MOST_SPECIAL + @string/charset_letters_and_digits_value - @string/charset_most_special_characters_value + @string/charset_most_special_value @@ -190,8 +194,8 @@ @string/charset_most_special_characters - @string/charset_most_special_characters_value - + @string/charset_most_special_value + downloads_max_retry 3 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7907d2974..10b36c1c7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -305,8 +305,7 @@ Allowed characters in filenames Invalid characters are replaced with this value Replacement character - [^\\w\\d]+ - [\\n\\r|\\?*<":>/']+ + Letters and digits Most special characters No app installed to play this file diff --git a/app/src/main/res/xml/download_settings.xml b/app/src/main/res/xml/download_settings.xml index bbb91acac..2f62aa89e 100644 --- a/app/src/main/res/xml/download_settings.xml +++ b/app/src/main/res/xml/download_settings.xml @@ -5,12 +5,6 @@ android:title="@string/settings_category_downloads_title"> - - !dT z{{J+J=)G!mKm7V>FnHTRNF~i)+rCPr6nwmtY!K5qdl-)CP?!~1PF_ql$K^u3@{HWrM2FYj7Y58toT^@550Zs&_6<*JEXEecIa`pN!& z(`MT$RgL25*U!INpEsAZec`pQLl;^KBIi_0d0fO%wuv@|FoaT=p(bJ0F~lQ}q6!dsue-)woM3=P zbS?kT>w)e0&^Zq}U5?X<;0vinn)IMEeGZcPHO^Tq%fK4<|1x!!;2d+8&Z04$Di3zr zHH?}om{>DmA-M9ISHX9-ItrF|Vb8%DCsFtoChh8`;zR`iQSg`)?UubZc%*Y4t<+$i z#7VlK6E)GLa+OV%YMR@0@lW-`<4n{i`)Tx)#_0~&+adqXq9-Q#gns#lvRAze*plYl zYbajAALEGsZ`u57I63-j#M>KatcJF)0J%5e)ScemTKjUSyXWaCe?(@!T(E0jbooyT z9(!Y`@?uLgk0XIX**K>z@fcQ=#M9Qmii2sj`JoQh;qkPZEO0(ryo{o?%YF)~5W%01 zwT#zeLW?(bFb=vLpHn8D_=E))q_uM9d(Y|?sCI?4J_qTYdA$_T#R&c1^}paWINdF$ zv-d%2kh)i-rcYE-5n#Lqwfjcxt++R4BA6u07QJ2sj-dDZ(>r84=^8^fxP1|BA5L-K zzov5=%zU{boF41h?@;c1#?jem#lX#l$-uc9>k9-aJ-BwIkCBN$u&$8Z1 z(O!r7U4yuJf!{!W_k-kjr2?D>sicx2y^HiN(z{6SBE5UP>D}#kjU+FUynPDQ7b2Mo zmZpK^7fc~QhC-;`O{i`(4%IEBsPL~;qhO*&;iJ?jEF!v!cq&n(fFcDHDWFIJMG7cVK#>B96tLY8B5~+U z(3!yQ53e&>O?4)d=uWnf!y%sKn_cQmdq|nijw+Ktbgw~1-kW>x|3>Rs-0m@VvY$wLQ6t$7gMLHMhT%>c6&b{7r?n~(njv_!>w^wOh9I%1lyob6| z=2r(?d3ryvqbEk|u1D)OB&YwdML->$OI3!UtuBGwvg9VMT`l{crv)D0{%0XPseTO~^iRZ8gnyDe>fyMy8sH z&CC^Oa`5O?Xgp;mEN`m35Zm*O6HR@`aL#Jl=oH*4K21fx<=E%o)4->JPt#+cMk7Rr zTXXX|kDhmH{01_Q(Y8laR%Y!t->cnqEYr?Tb=UN|`y^G|t38L5?V)A5zIA6<#H@{c z9ecI6V8W~0LrZ-t`I*y2<@E4@>*g%t zn4X?Vg05$GSvHpg$~Ldi{OqYkjJmj2v3W%c_N%seh2>zo3W%lG=*pi(e&ep zrdZGq?X-t+IGs#bnnF1Rv3o{r!4`~4Y>*0(>83=cV`~|7y@l;6HiC~q^iGAQ9gs$R z-SbDLMl9QKGGO1dZ+g3mg9bd>((p~c%JPJBnr`g+Ay+bBzX~j6z)}V*&%`>0cGfXy zEpAxhUuq?TZZtj)+_~%Y3~<%`$g+v+-s#F&xZyLuYul+xO3f#((}prwDZyqdm#hsi zsENVy^9_AV*6;w2!}hE&_zJ>jWDWgtY`UhM~u!!<$a zo>6+dQjgy?q*gTKuiixkE WHc!-_si5<(pZ*5+VQC&K{Qv;aZ3ZF$ literal 2520 zcmV;}2`Ba+iwFP!000021MOYga;rENeLr8}@_9`Z?w3v{T{F`))elqERWoOvDH#-- zJ{UYgB*!oF?Je2l0vO)_cSx42asdl1NoYx%rM<<^zx-GxlSjt$I7{!QI?<*RmWEjr zr}Mk%f4}|n+ME7z|K;Z>ra#G_Ij75sydurj(cN?*#OkM;o2REIlB{zoGES1XAUV7F zKTQ&PBNN?B@4rkYZ!?Ifpz2(CmI}e+Ss~aYrAu} zyXn`5@-w|DGrFndv?uo!owFHd^m}{en*8|wd6};muV=hmWqB-yRP>hp zx}WUpJzz6Oyk6e1$8B?KorEHf*~~s?5}Frr#PS23xtgXWr$w3U=1{xW({*f`D9%?2 zUGJ|#JyA&tN?v;ipu&tJ|Dg5 z@Y_3AcI00r*i81%$@S!$x_J6(j!R=aj_#(vYgH=L+fQaE&4C7;H_cd%dKS?P+r4s# z>pQMhXKJ(6+F)k0Ito{HPUi{RiF9k2@*jDuo5YeQ&O=@@L8k{s=O~t3I4gISuw@Wt zJZ&20eK+9-YBR?+oAG&m^@;2FZ$C~4tObkb3(*edkSdmMo(EQYVyQf@C6c{;zFfMIt7OZ$IA0F1hcedUORvqw8!4h;X zpH1dNS80|d(c9YOLjk=U=Z6M`&1f9Fq;F0s2cNua-AfueEf3#&CcU=XB8#{WE6LJ% zZ(z-Pe;jqr!P?!UkDJC%M@x$&+02`Vpc-b^=~zV?|F2*ds8qxrXpx9OW>>I&Z`w|a z=yD9o8GnpJ7F>`>at{CSJnEryze6JRfOIsIuJzGzso=@QNc5l0U+@S#`e-wf`W`d_ zjX&o2-iK&a11sEEC2(3qaUrVq-dla_2U zzNAU;kTq8X)Vaw6UB=1!Zo16UEML)(O((fntHn)iT4wQ&tl%-@=Pwx@M$c)QzW{qL z$iK1Vk%>5@*MHBF=)3}yd^)uZ%_96hOEz8VrsHimxLT(XPpf{cj%|MgWNm@)+~vr( zt$jIE!G4H9`PCG>xmHwl`P8&mZ=2IVHp_TJgFM7Zas)`-Gxm&{99W;$h+gZ2`T?-* z$*t2v2QLV{s~3b}6W?~+E0<`tZ+QcHLDZ0LO;a*gcdvZno{#(o1e_p7>;zGEWw=7X6#}9}7b2~AHs>tQ0})?v z2?`P)*aT4mq69<sTaW zkst^IJOYn;S(;V3Uy@A{^IM;gU=$ejUL3~@PA}>;j^$Yp=|es!t&)sJjKj(Ul|ZF# zPfLqpIZJ7rAlepug69xChu}E`&mnjYjmvY$?Cd#298+`sE8QlJuDL^c4potEb)6WN z?Oo|U(a%PH7oJ0XG~qFxL!Qjv?jgeHD9<5t2+tw4*>)1=0&yL=H(0Gy;tfS_>9| zMObJ8pTH-?F@Z{;(x-@4#~aQ-COAOA0Rj#XaDadV#Hbt~?5++Fx=l>OHdO?Us}H&R zaTVy+Fo>mTrV3#)^bzlU+(-Kz_dX`g9e)7Rzv5g3rPcX|Y(%&`z~y1YE)UP1J9rfn zTp!^2(1Q{I9zlkHrq_u{%B%`vX;1vX&f=JnKHiK3IS+Cki4l`8k z1KSt?r}URM@8&yq&Qt<>z-bg0`JQT?sdlGPKsnEz+UsN(9kUJ+md^LzBD*WZ!A@`{ zLM|v5+W|3%SRS_N=-q{SRhPp!dS{%!+~hyW*0pOUpm!W-n5C(lily0*HwK^QZzcaBK5;b7Q%IUbx;5o&Y$|iWB(^zf zsi&<8+mlL5#P)pSKvLf^oRgB8#2QxYIns97cOC2+*fp?gKDb?@5u(Gi8NJ-2S4|tg zgW6;CZ56e7v-{m&$>BPdX-^-!gsfw zpS&y8nbYmx!98b`hl^kl=hEm|!?hi*?IU(=hbJ!l2jPg@g+Za5eUyn5L{waEH?m** z1cO^U*z}3_#{2|s?O@U;WRiMJ!R|>Q5;lB6ga{%;5FvsHk@0N!qHCR=Ezy(nI?lx8sZDjBSq>NZ^pbKa2#9Jt?4sLviSVNHh#t zoZ{6Ro7qJH{|`q6sa i<;6ca?q_?P&^6=tU$zJGXU^&J{>z`GBdjay_y7Pr;@O)3 From 1089de6321ccce883fbd5bf9fe8a7c1c2db1febb Mon Sep 17 00:00:00 2001 From: kapodamy Date: Sun, 14 Apr 2019 22:51:59 -0300 Subject: [PATCH 15/30] Add confirm dialog before clear the finished download list --- .../us/shandian/giga/ui/fragment/MissionsFragment.java | 8 +++++++- app/src/main/res/values-es/strings.xml | 3 ++- app/src/main/res/values/strings.xml | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index bd5ce9215..f8cecbed9 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -1,6 +1,7 @@ package us.shandian.giga.ui.fragment; import android.app.Activity; +import android.app.AlertDialog; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -179,7 +180,12 @@ public class MissionsFragment extends Fragment { updateList(); return true; 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; case R.id.start_downloads: item.setVisible(false); diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 4cc394357..af5b8b213 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -427,7 +427,8 @@ abrir en modo popup Mostrar como grilla Mostrar como lista Limpiar descargas finalizadas - Continúa tus %s transferencias pendientes desde Descargas + ¿Estas seguro? + Tienes %s descargas pendientes, ve a Descargas para continuarlas Detener Intentos máximos Cantidad máxima de intentos antes de cancelar la descarga diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 10b36c1c7..9df9bd051 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -536,6 +536,7 @@ Progress lost, because the file was deleted Clear finished downloads + Are you sure? Continue your %s pending transfers from Downloads Stop Maximum retries From 4b3eb2ece58cf35a930293d6a1e60961337f28d9 Mon Sep 17 00:00:00 2001 From: kapodamy Date: Mon, 15 Apr 2019 21:31:33 -0300 Subject: [PATCH 16/30] Forget the download save path if the storage API is changed --- .../settings/DownloadSettingsFragment.java | 92 ++++++++++--------- .../giga/service/DownloadManagerService.java | 5 +- 2 files changed, 51 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java index 5d4ccf3f8..9fbf7dbea 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java @@ -2,6 +2,7 @@ package org.schabi.newpipe.settings; import android.app.Activity; import android.app.AlertDialog; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.net.Uri; @@ -22,6 +23,7 @@ import org.schabi.newpipe.util.FilePickerActivityHelper; import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.net.URI; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; @@ -30,7 +32,7 @@ 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_OLD_PATH = true; + public static final boolean IGNORE_RELEASE_ON_OLD_PATH = true; private String DOWNLOAD_PATH_VIDEO_PREFERENCE; private String DOWNLOAD_PATH_AUDIO_PREFERENCE; @@ -68,19 +70,14 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { if (javaIO == lastAPIJavaIO) return true; lastAPIJavaIO = javaIO; - boolean res; - - if (javaIO && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - // forget save paths (if necessary) - res = forgetPath(DOWNLOAD_PATH_VIDEO_PREFERENCE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + boolean res = forgetPath(DOWNLOAD_PATH_VIDEO_PREFERENCE); res |= forgetPath(DOWNLOAD_PATH_AUDIO_PREFERENCE); - } else { - res = hasInvalidPath(DOWNLOAD_PATH_VIDEO_PREFERENCE) || hasInvalidPath(DOWNLOAD_PATH_AUDIO_PREFERENCE); - } - if (res) { - Toast.makeText(ctx, R.string.download_pick_path, Toast.LENGTH_LONG).show(); - updatePreferencesSummary(); + if (res) { + Toast.makeText(ctx, R.string.download_pick_path, Toast.LENGTH_SHORT).show(); + updatePreferencesSummary(); + } } updatePathPickers(javaIO); @@ -88,25 +85,6 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { }); } - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - private boolean forgetPath(String prefKey) { - String path = defaultPreferences.getString(prefKey, ""); - if (path == null || path.isEmpty()) return true; - - if (path.startsWith("file://")) return false; - - // forget SAF path (file:// is compatible with the SAF wrapper) - forgetSAFTree(getContext(), prefKey); - defaultPreferences.edit().putString(prefKey, "").apply(); - - return true; - } - - private boolean hasInvalidPath(String prefKey) { - String value = defaultPreferences.getString(prefKey, null); - return value == null || value.isEmpty(); - } - @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { addPreferencesFromResource(R.xml.download_settings); @@ -137,6 +115,15 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { 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) { @@ -146,6 +133,24 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { target.setSummary(rawUri); } + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + private boolean forgetPath(String prefKey) { + String path = defaultPreferences.getString(prefKey, ""); + if (path == null || path.isEmpty()) return true; + + // forget SAF path if necessary + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + forgetSAFTree(getContext(), path); + + defaultPreferences.edit().putString(prefKey, "").apply(); + + return true; + } + + private boolean isFileUri(String path) { + return path.charAt(0) == File.separatorChar || path.startsWith(ContentResolver.SCHEME_FILE); + } + private void updatePathPickers(boolean useJavaIO) { boolean enabled = useJavaIO || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; prefPathVideo.setEnabled(enabled); @@ -159,25 +164,22 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { } // FIXME: after releasing the old path, all downloads created on the folder becomes inaccessible - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - private void forgetSAFTree(Context ctx, String prefKey) { - if (IGNORE_RELEASE_OLD_PATH) { + private void forgetSAFTree(Context ctx, String oldPath) { + if (IGNORE_RELEASE_ON_OLD_PATH) { return; } - String oldPath = defaultPreferences.getString(prefKey, ""); + if (oldPath == null || oldPath.isEmpty() || isFileUri(oldPath)) return; - if (oldPath != null && !oldPath.isEmpty() && oldPath.charAt(0) != File.separatorChar && !oldPath.startsWith("file://")) { - try { - Uri uri = Uri.parse(oldPath); + try { + Uri uri = Uri.parse(oldPath); - ctx.getContentResolver().releasePersistableUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS); - ctx.revokeUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS); + 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); - } + Log.i(TAG, "Revoke old path permissions success on " + oldPath); + } catch (Exception err) { + Log.e(TAG, "Error revoking old path permissions on " + oldPath, err); } } @@ -258,7 +260,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { final Context ctx = getContext(); if (ctx == null) throw new NullPointerException("getContext()"); - forgetSAFTree(ctx, key); + forgetSAFTree(ctx, defaultPreferences.getString(key, "")); try { ctx.grantUriPermission(ctx.getPackageName(), uri, StoredDirectoryHelper.PERMISSION_FLAGS); diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index da63cb545..8d838ccc2 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -566,7 +566,10 @@ public class DownloadManagerService extends Service { } if (path == null || path.isEmpty()) { - return useJavaIO ? new StoredDirectoryHelper(new File(defaultPath).toURI(), tag) : null; + if (useJavaIO) + return new StoredDirectoryHelper(new File(defaultPath).toURI(), tag); + else + return null; } if (path.charAt(0) == File.separatorChar) { From 16d6bda85d7c487086c574e44250badd315a6cdf Mon Sep 17 00:00:00 2001 From: kapodamy Date: Tue, 16 Apr 2019 23:28:03 -0300 Subject: [PATCH 17/30] Webm muxer fixes and strings.xml changes * replace "In queue" to "Pending" in the downloads header to avoid confusions (all languages) * use 29bits Clusters size to support huge video resolutions (fixes #2291) (WebmWriter.java) * add missing changes to WebmMuxer.java (i forget select the audio track) --- .../org/schabi/newpipe/streams/WebMWriter.java | 7 +++---- .../giga/postprocessing/WebMMuxer.java | 18 +++++++++++------- app/src/main/res/values-ar/strings.xml | 2 +- app/src/main/res/values-ca/strings.xml | 2 +- app/src/main/res/values-cmn/strings.xml | 2 +- app/src/main/res/values-da/strings.xml | 1 + app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values-eu/strings.xml | 2 +- app/src/main/res/values-he/strings.xml | 2 +- app/src/main/res/values-id/strings.xml | 2 +- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-ja/strings.xml | 6 +++--- app/src/main/res/values-ms/strings.xml | 2 +- app/src/main/res/values-nb-rNO/strings.xml | 1 - app/src/main/res/values-nl-rBE/strings.xml | 1 - app/src/main/res/values-nl/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pt-rBR/strings.xml | 2 +- app/src/main/res/values-pt/strings.xml | 2 +- app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-sq/strings.xml | 1 + app/src/main/res/values-tr/strings.xml | 2 +- app/src/main/res/values-vi/strings.xml | 2 +- app/src/main/res/values-zh-rTW/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 26 files changed, 38 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java index 26b9cbebf..98261b0c9 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java @@ -321,9 +321,8 @@ public class WebMWriter { for (int i = 0; i < clusterSizes.size(); i++) { seekTo(out, clusterOffsets.get(i)); - byte[] size = ByteBuffer.allocate(4).putInt(clusterSizes.get(i) | 0x200000).array(); - out.write(size, 1, 3); - written += 3; + byte[] buffer = ByteBuffer.allocate(4).putInt(clusterSizes.get(i) | 0x10000000).array(); + dump(buffer, out); } } @@ -451,7 +450,7 @@ public class WebMWriter { /* cluster */ dump(new byte[]{0x1f, 0x43, (byte) 0xb6, 0x75}, stream); 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 diff --git a/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java index 3d5ecb3cd..618c1ec5a 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java @@ -22,16 +22,20 @@ class WebMMuxer extends Postprocessing { muxer.parseSources(); // youtube uses a webm with a fake video track that acts as a "cover image" - WebMTrack[] tracks = muxer.getTracksFromSource(1); - int audioTrackIndex = 0; - for (int i = 0; i < tracks.length; i++) { - if (tracks[i].kind == TrackKind.Audio) { - audioTrackIndex = i; - break; + int[] indexes = new int[sources.length]; + + for (int i = 0; i < sources.length; i++) { + WebMTrack[] tracks = muxer.getTracksFromSource(i); + for (int j = 0; j < tracks.length; j++) { + if (tracks[j].kind == TrackKind.Audio) { + indexes[i] = j; + i = sources.length; + break; + } } } - muxer.selectTracks(0, audioTrackIndex); + muxer.selectTracks(indexes); muxer.build(out); return OK_RESULT; diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index dbf015c87..cc36e40bf 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -451,7 +451,7 @@ يتوفر تحديث ل newpipe! اضغط لتنزيل انتهى - في قائمة الانتظار + ريثما متوقف في قائمة الانتظار قيد المعالجة diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index f606281f4..04bb36ea3 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -425,7 +425,7 @@ Automàtic Canvia la vista Està disponible una nova actualització del NewPipe! - A la cua + Pendent en pausa a la cua Afegeix a la cua diff --git a/app/src/main/res/values-cmn/strings.xml b/app/src/main/res/values-cmn/strings.xml index 73eb43c36..7be9efc04 100644 --- a/app/src/main/res/values-cmn/strings.xml +++ b/app/src/main/res/values-cmn/strings.xml @@ -426,7 +426,7 @@ 自动 轻按以下载 已完成 - 于队列中 + 有待 已暂停 已加入队列 后处理 diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 92535310e..0c699cf0e 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -453,4 +453,5 @@ Maksimalt antal forsøg før downloaden opgives Sæt på pause ved skift til mobildata Downloads som ikke kan sættes på pause vil blive genstartet + Afventning \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index ef2789846..ae4fda922 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -437,7 +437,7 @@ NewPipe-Aktualisierung verfügbar! Zum Herunterladen antippen Fertig - In der Warteschlange + Ausstehend pausiert eingereiht Nachbearbeitung diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index af5b8b213..6a493892d 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -469,6 +469,7 @@ abrir en modo popup No es posible descargar a una tarjeta SD externa. \¿Restablecer la ubicación de la carpeta de descarga\? Seleccione los directorios de descarga + Pendiente Desuscribirse Nueva pestaña diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 2868528e9..fb41bf8ae 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -435,7 +435,7 @@ NewPipe eguneraketa eskuragarri! Sakatu deskargatzeko Amaituta - Ilaran + Zain pausatuta ilaran post-prozesua diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 41569ff0c..99f02dde8 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -440,7 +440,6 @@ יצא עדכון ל־NewPipe! יש לגעת כדי להוריד הסתיים - בתור מושהה בתור עיבוד מאוחר @@ -473,4 +472,5 @@ מספר הניסיונות החוזרים המרבי בטרם ביטול ההורדה להשהות בעת מעבר לתקשורת נתונים סלולרית הורדות שלא ניתן להשהות יופעלו מחדש + בהמתנה \ No newline at end of file diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 31801434b..c1eb3870d 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -436,7 +436,7 @@ Pembaruan NewPipe Tersedia! Ketuk untuk mengunduh Selesai - Di antrian + Tertunda dijeda antri pengolahan-pasca diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 4ff8de734..f6d6e42f7 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -438,7 +438,7 @@ Aggiornamento di NewPipe disponibile! Premi per scaricare Finito - In coda + In attesa di in pausa in coda post-processo diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 78a20b1ab..76ccfd2dd 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -421,7 +421,7 @@ NewPipeのアップデートがあります! タップでダウンロード 終了しました - 順番に処理中 + 保留中 一時停止 順番待ちに追加しました 保存処理をしています @@ -462,6 +462,6 @@ メインページに表示されるタブ 新しいバージョンが利用可能なときにアプリの更新を確認する通知を表示します ダウンロードから %s の保留中の転送を続行します - モバイルデータ通信に切替時に、一時停止する - 一時停止できない場合は再開して継続されます + モバイルデータ通信に切り替え時に休止 + 休止できないダウンロードが再開されます \ No newline at end of file diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index ced8235f7..1f0dc3968 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -432,7 +432,6 @@ Kemas kini NewPipe Tersedia! Ketik untuk muat turun Selesai - Dalam barisan dijeda telah beratur pemprosesan-pasca @@ -465,4 +464,5 @@ Jumlah percubaan maksimum sebelum membatalkan muat turun Jeda semasa beralih ke data mudah alih Muat turun yang tidak dapat dihentikan akan dimulakan semula + Menunggu \ No newline at end of file diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 2f5d19c67..1c81feae5 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -515,7 +515,6 @@ Ny NewPipe-versjon tilgjengelig. Trykk for å laste ned Fullført - I kø pauset i kø etterbehandling diff --git a/app/src/main/res/values-nl-rBE/strings.xml b/app/src/main/res/values-nl-rBE/strings.xml index 44b2ef6ab..eac4114ff 100644 --- a/app/src/main/res/values-nl-rBE/strings.xml +++ b/app/src/main/res/values-nl-rBE/strings.xml @@ -434,7 +434,6 @@ NewPipe-update beschikbaar! Tikt voor te downloaden Voltooid - In wachtrij gepauzeerd toegevoegd aan wachtrij nabewerking diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 96de68b57..4e88b6b48 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -438,7 +438,7 @@ NewPipe-update beschikbaar! Tik om te downloaden Voltooid - In de wachtrij + In afwachting van gepauzeerd aan de wachtrij toegevoegd nabewerking diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 29070990f..d4a56256e 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -435,7 +435,6 @@ Dostępna jest aktualizacja NewPipe! Stuknij, aby pobrać Gotowe - W kolejce wstrzymane w kolejce przetwarzanie końcowe @@ -470,4 +469,5 @@ Pobierane pliki, których nie można wstrzymać, zostaną zrestartowane Zdarzenia Konferencje + Oczekuje \ No newline at end of file diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 8a16b752d..097ad1288 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -435,7 +435,6 @@ abrir em modo popup Atualização do NewPipe Disponivel! Toque para baixar Finalizado - Na fila pausado adicionado na fila pós processamento @@ -468,4 +467,5 @@ abrir em modo popup Número máximo de tentativas antes de cancelar o download Pausar quando trocar para dados móveis Downloads que não puderem ser pausados serão reiniciados + Pendente \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index a86c5b809..ddc9d503c 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -431,7 +431,6 @@ Atualização do NewPipe disponível! Toque para descarregar Terminada - Na fila em pausa na fila pós-processamento @@ -466,4 +465,5 @@ Descarregamentos que não podem ser pausados serão reiniciados Eventos Conferências + Pendente \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 620ca5619..374b9921f 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -472,4 +472,5 @@ Пост-обработка не удалась Останавливать скачивание при переходе на мобильную сеть Закрыть + в ожидании \ No newline at end of file diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml index ec31c4a97..74bf10804 100644 --- a/app/src/main/res/values-sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -48,4 +48,5 @@ Po Më vonë Standard + në pritje të diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index e518a1c0f..dd7974af2 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -438,7 +438,6 @@ NewPipe Güncellemesi Var! İndirmek için dokunun Tamamlandı - Sırada durdurulmuş sırada son işlemler uygulanıyor @@ -473,4 +472,5 @@ Duraklatılamayan indirmeler yeniden başlatılacak Olaylar Konferanslar + Kadar \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index ff247c579..4c9f9c7d0 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -429,7 +429,6 @@ Đã có bản cập nhật NewPipe! Nhấn để tải về Xong - Trong hàng chờ đã tạm dừng trong hàng đợi đang xử lý @@ -461,6 +460,7 @@ Số lượt thử lại trước khi hủy tải về Tạm dừng tải khi chuyển qua dữ liệu di động Các tải về không thể tạm dừng được sẽ bắt đầu lại từ đầu + Đang chờ xử lý Hội thảo \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 0194418cf..023cd00c8 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -434,7 +434,7 @@ 有可用的 NewPipe 更新! 輕觸以下載 結束 - 在佇列中 + 有待 已暫停 已排入佇列 正在後處理 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9df9bd051..8433f909d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -498,7 +498,7 @@ NewPipe Update Available! Tap to download Finished - In queue + Pending paused queued post-processing From d1573a0a6e9e409df3bb8c780a4bf2222dca6272 Mon Sep 17 00:00:00 2001 From: kapodamy Date: Wed, 17 Apr 2019 18:17:24 -0300 Subject: [PATCH 18/30] misc changes * implement socket timeout error * use 128k buffer size for copy * use NewPipe HTTP user agent in the downloads * automatically recover downloads with network errors that are queued --- .../giga/get/DownloadInitializer.java | 2 +- .../us/shandian/giga/get/DownloadMission.java | 25 ++++++++++++++++--- .../shandian/giga/get/DownloadRunnable.java | 1 - .../giga/get/DownloadRunnableFallback.java | 11 ++++---- .../shandian/giga/io/CircularFileWriter.java | 7 +++--- .../giga/ui/adapter/MissionAdapter.java | 4 +++ app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values-eu/strings.xml | 1 + app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values-it/strings.xml | 1 + app/src/main/res/values-ja/strings.xml | 2 ++ app/src/main/res/values-nl/strings.xml | 1 + app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 16 files changed, 46 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java index f6b6b459a..590c3704c 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java @@ -28,7 +28,7 @@ public class DownloadInitializer extends Thread { @Override public void run() { - if (mMission.current > 0) mMission.resetState(false,true, DownloadMission.ERROR_NOTHING); + if (mMission.current > 0) mMission.resetState(false, true, DownloadMission.ERROR_NOTHING); int retryCount = 0; while (true) { diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMission.java b/app/src/main/java/us/shandian/giga/get/DownloadMission.java index 838acc162..9bc46e3af 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -4,11 +4,14 @@ import android.os.Handler; import android.os.Message; import android.util.Log; +import org.schabi.newpipe.Downloader; + import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.net.ConnectException; import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; import java.net.URL; import java.net.UnknownHostException; import java.util.ArrayList; @@ -45,6 +48,7 @@ public class DownloadMission extends Mission { 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_UNSUPPORTED_RANGE = 206; @@ -232,10 +236,9 @@ public class DownloadMission extends Mission { URL url = new URL(urls[current]); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setInstanceFollowRedirects(true); + conn.setRequestProperty("User-Agent", Downloader.USER_AGENT); // BUG workaround: switching between networks can freeze the download forever - - //conn.setRequestProperty("Connection", "close"); conn.setConnectTimeout(30000); conn.setReadTimeout(10000); @@ -329,6 +332,8 @@ public class DownloadMission extends Mission { notifyError(ERROR_CONNECT_HOST, null); } else if (err instanceof UnknownHostException) { notifyError(ERROR_UNKNOWN_HOST, null); + } else if (err instanceof SocketTimeoutException) { + notifyError(ERROR_TIMEOUT, null); } else { notifyError(ERROR_UNKNOWN_EXCEPTION, err); } @@ -338,7 +343,7 @@ public class DownloadMission extends Mission { Log.e(TAG, "notifyError() code = " + code, err); if (err instanceof IOException) { - if (storage.canWrite() || err.getMessage().contains("Permission denied")) { + if (!storage.canWrite() || err.getMessage().contains("Permission denied")) { code = ERROR_PERMISSION_DENIED; err = null; } else if (err.getMessage().contains("ENOSPC")) { @@ -349,7 +354,19 @@ public class DownloadMission extends Mission { errCode = code; errObject = err; - enqueued = false; + + 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(); diff --git a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java index 7a68cd778..4380c0c68 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java @@ -27,7 +27,6 @@ public class DownloadRunnable extends Thread { if (mission == null) throw new NullPointerException("mission is null"); mMission = mission; mId = id; - mConn = null; } @Override diff --git a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java index 1a4b5d5b6..d7ff208ce 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java @@ -1,6 +1,5 @@ package us.shandian.giga.get; -import android.annotation.SuppressLint; import android.support.annotation.NonNull; import android.util.Log; @@ -19,7 +18,7 @@ import static org.schabi.newpipe.BuildConfig.DEBUG; * Single-threaded fallback mode */ public class DownloadRunnableFallback extends Thread { - private static final String TAG = "DownloadRunnableFallback"; + private static final String TAG = "DownloadRunnableFallbac"; private final DownloadMission mMission; @@ -30,9 +29,6 @@ public class DownloadRunnableFallback extends Thread { DownloadRunnableFallback(@NonNull DownloadMission mission) { mMission = mission; - mIs = null; - mF = null; - mConn = null; } private void dispose() { @@ -46,7 +42,6 @@ public class DownloadRunnableFallback extends Thread { } @Override - @SuppressLint("LongLogTag") public void run() { boolean done; @@ -106,6 +101,10 @@ public class DownloadRunnableFallback extends Thread { return; } + if (DEBUG) { + Log.e(TAG, "got exception, retrying...", e); + } + run();// try again return; } diff --git a/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java b/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java index 327b9149e..f9ceca6ad 100644 --- a/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java +++ b/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java @@ -11,6 +11,7 @@ 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 @@ -53,6 +54,7 @@ public class CircularFileWriter extends SharpStream { 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); @@ -60,14 +62,14 @@ public class CircularFileWriter extends SharpStream { long length = amount; while (length > 0) { int read = (int) Math.min(length, Integer.MAX_VALUE); - read = aux.target.read(aux.queue, 0, Math.min(read, aux.queue.length)); + read = aux.target.read(buffer, 0, Math.min(read, buffer.length)); if (read < 1) { amount -= length; break; } - out.writeProof(aux.queue, read); + out.writeProof(buffer, read); length -= read; } @@ -100,7 +102,6 @@ public class CircularFileWriter extends SharpStream { // move the excess data to the beginning of the file long readOffset = amount; long writeOffset = 0; - byte[] buffer = new byte[128 * 1024]; // 128 KiB aux.length -= amount; length = aux.length; diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index 1892f4437..21cae1835 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -73,6 +73,7 @@ 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_TIMEOUT; import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST; @@ -487,6 +488,9 @@ public class MissionAdapter extends Adapter { case ERROR_PROGRESS_LOST: msg = R.string.error_progress_lost; break; + case ERROR_TIMEOUT: + msg = R.string.error_timeout; + break; default: if (mission.errCode >= 100 && mission.errCode < 600) { msgEx = "HTTP " + mission.errCode; diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index ae4fda922..9a2518652 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -472,4 +472,5 @@ Downloads, die nicht pausiert werden können, werden wiederholt Konferenzen Ereignisse + Verbindungszeitüberschreitung \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 6a493892d..2e8c62bce 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -457,6 +457,7 @@ abrir en modo popup NewPipe se cerro mientras se trabajaba en el archivo No hay suficiente espacio disponible en el dispositivo Se perdió el progreso porque el archivo fue eliminado + Tiempo de espera excedido API de almacenamiento Seleccione que API utilizar para almacenar las descargas diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index fb41bf8ae..11fed8e64 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -468,4 +468,5 @@ Deskarga ezeztatu aurretik saiatu beharreko aldi kopurua Pausatu datu mugikorretara aldatzean Pausatu ezin daitezkeen deskargak berrekingo dira + Konexioaren denbora muga \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index b81195d41..ca9682fdb 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -440,6 +440,7 @@ Dans la file d\'attente En pause Téléchargement échoué + Délai de connection dépassé Conférences Téléchargement terminé %s téléchargements terminés diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f6d6e42f7..d61b26cfb 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -473,4 +473,5 @@ I download che non possono essere messi in pausa verranno riavviati Eventi Conferenze + Connesione finita \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 76ccfd2dd..be243bffd 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -464,4 +464,6 @@ ダウンロードから %s の保留中の転送を続行します モバイルデータ通信に切り替え時に休止 休止できないダウンロードが再開されます + 保留中 + 接続タイムアウト \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 4e88b6b48..915111043 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -473,4 +473,5 @@ Downloads die niet kunnen worden gepauzeerd zullen worden herstart Gebeurtenissen Conferenties + Time-out van verbinding \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 374b9921f..c75af14e2 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -473,4 +473,5 @@ Останавливать скачивание при переходе на мобильную сеть Закрыть в ожидании + Время соединения вышло \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 023cd00c8..9b58bb79f 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -469,4 +469,5 @@ 無法暫停的下載將會重新開始 事件 會議 + 連接超時 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8433f909d..f10a9f589 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -534,6 +534,7 @@ NewPipe was closed while working on the file No space left on device Progress lost, because the file was deleted + Connection timeout Clear finished downloads Are you sure? From 34b2b9615832a9e1846b6dcfd2b8b6a0e656f162 Mon Sep 17 00:00:00 2001 From: kapodamy Date: Fri, 19 Apr 2019 16:18:19 -0300 Subject: [PATCH 19/30] Simplify the storage APIs use * use Java I/O (classic way) on older android versions * use Storage Access Framework on newer android versions (Android Lollipop or later) * both changes have the external SD Card write permission * add option to ask the save path on each download * warn the user if the save paths are not defined, this only happens on the first NewPipe run (Android Lollipop or later) --- .../newpipe/download/DownloadDialog.java | 5 +- .../settings/DownloadSettingsFragment.java | 117 +++++++----------- .../newpipe/settings/NewPipeSettings.java | 19 +-- .../giga/io/StoredDirectoryHelper.java | 5 +- .../giga/service/DownloadManagerService.java | 104 ++++++++-------- app/src/main/res/values-es/strings.xml | 12 +- app/src/main/res/values/settings_keys.xml | 15 +-- app/src/main/res/values/strings.xml | 12 +- app/src/main/res/xml/download_settings.xml | 12 +- 9 files changed, 119 insertions(+), 182 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 8f4b569cd..f27e7467e 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -212,6 +212,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck mainStorageAudio = mgr.getMainStorageAudio(); mainStorageVideo = mgr.getMainStorageVideo(); downloadManager = mgr.getDownloadManager(); + askForSavePath = mgr.askForSavePath(); okButton.setEnabled(true); @@ -509,6 +510,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck DownloadManager downloadManager = null; ActionMenuItemView okButton = null; Context context; + boolean askForSavePath; private String getNameEditText() { String str = nameEditText.getText().toString().trim(); @@ -567,10 +569,11 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck throw new RuntimeException("No stream selected"); } - if (mainStorage == null) { + 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; diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java index 9fbf7dbea..e671e4d3a 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java @@ -9,7 +9,6 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.support.annotation.Nullable; -import android.support.annotation.RequiresApi; import android.support.annotation.StringRes; import android.support.v7.preference.Preference; import android.util.Log; @@ -37,50 +36,40 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { private String DOWNLOAD_PATH_VIDEO_PREFERENCE; private String DOWNLOAD_PATH_AUDIO_PREFERENCE; - private String DOWNLOAD_STORAGE_API; - private String DOWNLOAD_STORAGE_API_DEFAULT; + private String DOWNLOAD_STORAGE_ASK; private Preference prefPathVideo; private Preference prefPathAudio; + private Preference prefStorageAsk; private Context ctx; - private boolean lastAPIJavaIO; - @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); DOWNLOAD_PATH_VIDEO_PREFERENCE = getString(R.string.download_path_video_key); DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key); - DOWNLOAD_STORAGE_API = getString(R.string.downloads_storage_api); - DOWNLOAD_STORAGE_API_DEFAULT = getString(R.string.downloads_storage_api_default); + DOWNLOAD_STORAGE_ASK = getString(R.string.downloads_storage_ask); prefPathVideo = findPreference(DOWNLOAD_PATH_VIDEO_PREFERENCE); prefPathAudio = findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE); - - lastAPIJavaIO = usingJavaIO(); + prefStorageAsk = findPreference(DOWNLOAD_STORAGE_ASK); updatePreferencesSummary(); - updatePathPickers(lastAPIJavaIO); + updatePathPickers(!defaultPreferences.getBoolean(DOWNLOAD_STORAGE_ASK, false)); - findPreference(DOWNLOAD_STORAGE_API).setOnPreferenceChangeListener((preference, value) -> { - boolean javaIO = DOWNLOAD_STORAGE_API_DEFAULT.equals(value); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + prefStorageAsk.setSummary(R.string.downloads_storage_ask_summary); + } - if (javaIO == lastAPIJavaIO) return true; - lastAPIJavaIO = javaIO; + if (hasInvalidPath(DOWNLOAD_PATH_VIDEO_PREFERENCE) || hasInvalidPath(DOWNLOAD_PATH_AUDIO_PREFERENCE)) { + Toast.makeText(ctx, R.string.download_pick_path, Toast.LENGTH_SHORT).show(); + updatePreferencesSummary(); + } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - boolean res = forgetPath(DOWNLOAD_PATH_VIDEO_PREFERENCE); - res |= forgetPath(DOWNLOAD_PATH_AUDIO_PREFERENCE); - - if (res) { - Toast.makeText(ctx, R.string.download_pick_path, Toast.LENGTH_SHORT).show(); - updatePreferencesSummary(); - } - } - - updatePathPickers(javaIO); + prefStorageAsk.setOnPreferenceChangeListener((preference, value) -> { + updatePathPickers(!(boolean) value); return true; }); } @@ -100,7 +89,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { public void onDetach() { super.onDetach(); ctx = null; - findPreference(DOWNLOAD_STORAGE_API).setOnPreferenceChangeListener(null); + prefStorageAsk.setOnPreferenceChangeListener(null); } private void updatePreferencesSummary() { @@ -133,34 +122,18 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { target.setSummary(rawUri); } - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - private boolean forgetPath(String prefKey) { - String path = defaultPreferences.getString(prefKey, ""); - if (path == null || path.isEmpty()) return true; - - // forget SAF path if necessary - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) - forgetSAFTree(getContext(), path); - - defaultPreferences.edit().putString(prefKey, "").apply(); - - return true; - } - private boolean isFileUri(String path) { return path.charAt(0) == File.separatorChar || path.startsWith(ContentResolver.SCHEME_FILE); } - private void updatePathPickers(boolean useJavaIO) { - boolean enabled = useJavaIO || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; - prefPathVideo.setEnabled(enabled); - prefPathAudio.setEnabled(enabled); + private boolean hasInvalidPath(String prefKey) { + String value = defaultPreferences.getString(prefKey, null); + return value == null || value.isEmpty(); } - private boolean usingJavaIO() { - return DOWNLOAD_STORAGE_API_DEFAULT.equals( - defaultPreferences.getString(DOWNLOAD_STORAGE_API, DOWNLOAD_STORAGE_API_DEFAULT) - ); + 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 @@ -198,33 +171,31 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { } String key = preference.getKey(); + int request; - if (key.equals(DOWNLOAD_PATH_VIDEO_PREFERENCE) || key.equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) { - boolean safPick = !usingJavaIO(); - - int request = 0; - if (key.equals(DOWNLOAD_PATH_VIDEO_PREFERENCE)) { - request = REQUEST_DOWNLOAD_VIDEO_PATH; - } else if (key.equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) { - request = REQUEST_DOWNLOAD_AUDIO_PATH; - } - - Intent i; - if (safPick && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - .putExtra("android.content.extra.SHOW_ADVANCED", true) - .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS); - } else { - i = new Intent(getActivity(), FilePickerActivityHelper.class) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) - .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_DIR); - } - - startActivityForResult(i, request); + 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); } - 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_CREATE_DIR, true) + .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_DIR); + } + + startActivityForResult(i, request); + + return true; } @Override @@ -252,7 +223,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { return; } - if (!usingJavaIO() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + 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 diff --git a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java index f153cf23a..f18d90a95 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java +++ b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java @@ -94,24 +94,7 @@ public class NewPipeSettings { return new File(Environment.getExternalStorageDirectory(), defaultDirectoryName); } - public static void resetDownloadFolders(Context context) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - - prefs.edit() - .putString(context.getString(R.string.downloads_storage_api), context.getString(R.string.downloads_storage_api_default)) - .apply(); - - resetDownloadFolder(prefs, context.getString(R.string.download_path_audio_key), Environment.DIRECTORY_MUSIC); - resetDownloadFolder(prefs, context.getString(R.string.download_path_video_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) { - return new File(dir, "NewPipe").getAbsolutePath(); + return new File(dir, "NewPipe").toURI().toString(); } } diff --git a/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java b/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java index eb3c9b817..a65c4dff3 100644 --- a/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java +++ b/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java @@ -10,7 +10,6 @@ import android.os.Build; import android.provider.DocumentsContract; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.annotation.RequiresApi; import android.support.v4.provider.DocumentFile; import java.io.File; @@ -33,7 +32,6 @@ public class StoredDirectoryHelper { private String tag; - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) public StoredDirectoryHelper(@NonNull Context context, @NonNull Uri path, String tag) throws IOException { this.tag = tag; @@ -50,6 +48,9 @@ public class StoredDirectoryHelper { 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) diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index 8d838ccc2..f25147507 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -1,12 +1,12 @@ package us.shandian.giga.service; import android.Manifest; +import android.app.AlertDialog; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -20,7 +20,6 @@ import android.net.NetworkRequest; import android.net.Uri; import android.os.Binder; import android.os.Build; -import android.os.Environment; import android.os.Handler; import android.os.IBinder; import android.os.Looper; @@ -28,6 +27,7 @@ import android.os.Message; 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.Builder; import android.support.v4.content.PermissionChecker; @@ -41,7 +41,6 @@ import org.schabi.newpipe.player.helper.LockManager; import java.io.File; import java.io.IOException; -import java.net.URI; import java.util.ArrayList; import us.shandian.giga.get.DownloadMission; @@ -141,7 +140,7 @@ public class DownloadManagerService extends Service { mPrefs = PreferenceManager.getDefaultSharedPreferences(this); - mManager = new DownloadManager(this, mHandler, getVideoStorage(), getAudioStorage()); + mManager = new DownloadManager(this, mHandler, loadMainVideoStorage(), loadMainAudioStorage()); Intent openDownloadListIntent = new Intent(this, DownloadActivity.class) .setAction(Intent.ACTION_MAIN); @@ -271,6 +270,33 @@ public class DownloadManagerService extends Service { Toast.makeText(this, "Permission denied (write)", Toast.LENGTH_SHORT).show(); } + // Check download save paths + + String msg = ""; + if (mManager.mMainStorageVideo == null) + msg += getString(R.string.download_path_title); + else if (mManager.mMainStorageAudio == null) + msg += getString(R.string.download_path_audio_title); + + if (!msg.isEmpty()) { + String title; + if (mManager.mMainStorageVideo == null && mManager.mMainStorageAudio == 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(this) + .setPositiveButton(android.R.string.ok, null) + .setTitle(title) + .setMessage(msg) + .create() + .show(); + } + + return mBinder; } @@ -348,13 +374,10 @@ public class DownloadManagerService extends Service { 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.downloads_storage_api))) { - mManager.mMainStorageVideo = loadMainStorage(getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO); - mManager.mMainStorageAudio = loadMainStorage(getString(R.string.download_path_audio_key), DownloadManager.TAG_AUDIO); } else if (key.equals(getString(R.string.download_path_video_key))) { - mManager.mMainStorageVideo = loadMainStorage(key, DownloadManager.TAG_VIDEO); + mManager.mMainStorageVideo = loadMainVideoStorage(); } else if (key.equals(getString(R.string.download_path_audio_key))) { - mManager.mMainStorageAudio = loadMainStorage(key, DownloadManager.TAG_AUDIO); + mManager.mMainStorageAudio = loadMainAudioStorage(); } } @@ -385,7 +408,7 @@ public class DownloadManagerService extends Service { * @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, + public static void startMission(Context context, String[] urls, StoredFileHelper storage, char kind, int threads, String source, String psName, String[] psArgs, long nearLength) { Intent intent = new Intent(context, DownloadManagerService.class); intent.setAction(Intent.ACTION_RUN); @@ -538,56 +561,28 @@ public class DownloadManagerService extends Service { mLockAcquired = acquire; } - private StoredDirectoryHelper getVideoStorage() { - return loadMainStorage(getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO); + private StoredDirectoryHelper loadMainVideoStorage() { + return loadMainStorage(R.string.download_path_video_key, DownloadManager.TAG_VIDEO); } - private StoredDirectoryHelper getAudioStorage() { - return loadMainStorage(getString(R.string.download_path_audio_key), DownloadManager.TAG_AUDIO); + 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); - private StoredDirectoryHelper loadMainStorage(String prefKey, String tag) { - String path = mPrefs.getString(prefKey, null); - - final String JAVA_IO = getString(R.string.downloads_storage_api_default); - boolean useJavaIO = JAVA_IO.equals(mPrefs.getString(getString(R.string.downloads_storage_api), JAVA_IO)); - - final String defaultPath; - switch (tag) { - case DownloadManager.TAG_VIDEO: - defaultPath = Environment.DIRECTORY_MOVIES; - break; - case DownloadManager.TAG_AUDIO: - defaultPath = Environment.DIRECTORY_MUSIC; - break; - default: - return null; - } - - if (path == null || path.isEmpty()) { - if (useJavaIO) - return new StoredDirectoryHelper(new File(defaultPath).toURI(), tag); - else - return null; - } + if (path == null || path.isEmpty()) return null; if (path.charAt(0) == File.separatorChar) { - Log.i(TAG, "Migrating old save path: " + path); + Log.i(TAG, "Old save path style present: " + path); - useJavaIO = true; - path = Uri.fromFile(new File(path)).toString(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + path = Uri.fromFile(new File(path)).toString(); + else + path = ""; - mPrefs.edit().putString(prefKey, path).apply(); - } - - boolean override = path.startsWith(ContentResolver.SCHEME_FILE) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; - if (useJavaIO || override) { - return new StoredDirectoryHelper(URI.create(path), tag); - } - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - return null;// SAF Directory API is not available in older versions + mPrefs.edit().putString(getString(prefKey), "").apply(); } try { @@ -619,6 +614,13 @@ public class DownloadManagerService extends Service { 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) { manageObservers(handler, true); } diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 2e8c62bce..890755845 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -459,19 +459,15 @@ abrir en modo popup Se perdió el progreso porque el archivo fue eliminado Tiempo de espera excedido - API de almacenamiento - Seleccione que API utilizar para almacenar las descargas - - Framework de acceso a almacenamiento - Java I/O - - Guardar como… - No es posible descargar a una tarjeta SD externa. \¿Restablecer la ubicación de la carpeta de descarga\? Seleccione los directorios de descarga Pendiente + Preguntar dónde descargar + Se preguntará dónde guardar cada descarga + Se preguntará dónde guardar cada descarga.\nHabilita esta opción si quieres descargar en la tarjeta SD externa + Desuscribirse Nueva pestaña Elige la pestaña diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 42df857c1..ec288bb18 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -160,20 +160,7 @@ clear_play_history clear_search_history - downloads_storage_api - - - javaIO - - - SAF - javaIO - - - - @string/storage_access_framework_description - @string/java_io_description - + downloads_storage_ask file_rename_charset diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f10a9f589..fb154313e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -550,14 +550,10 @@ Start downloads Pause downloads - Storage API - Select which API use to store the downloads - - Storage Access Framework - Java I/O - - Save as… - Select the downloads save path + Ask where to download + You will be asked where to save each download + You will be asked where to save each download.\nEnable this option if you want download to the external SD Card + \ No newline at end of file diff --git a/app/src/main/res/xml/download_settings.xml b/app/src/main/res/xml/download_settings.xml index 2f62aa89e..7a6fab841 100644 --- a/app/src/main/res/xml/download_settings.xml +++ b/app/src/main/res/xml/download_settings.xml @@ -5,14 +5,12 @@ android:title="@string/settings_category_downloads_title"> - + android:defaultValue="false" + android:key="@string/downloads_storage_ask" + android:summary="@string/downloads_storage_ask_summary_kitkat" + android:title="@string/downloads_storage_ask_title" /> Date: Thu, 25 Apr 2019 00:34:29 -0300 Subject: [PATCH 20/30] Space reserving tweaks for huge video resolutions * improve space reserving, allows write better 4K/8K video data * do not use cache dirs in the muxers, Android can force close NewPipe if the device is running out of storage. Is a aggressive cache cleaning >:/ * (for devs) webm & mkv are the same thing * calculate the final file size inside of the mission, instead getting from the UI * simplify ps algorithms constructors * [missing old commit message] simplify the loading of pending downloads --- .../newpipe/download/DownloadDialog.java | 5 +- .../giga/get/DownloadInitializer.java | 349 ++++++------ .../us/shandian/giga/get/DownloadMission.java | 30 +- .../giga/postprocessing/M4aNoDash.java | 4 +- .../giga/postprocessing/Mp4FromDashMuxer.java | 2 +- .../giga/postprocessing/Postprocessing.java | 503 +++++++++--------- .../giga/postprocessing/TtmlConverter.java | 144 ++--- .../giga/postprocessing/WebMMuxer.java | 88 +-- .../giga/service/DownloadManager.java | 25 +- .../giga/service/DownloadManagerService.java | 5 +- .../java/us/shandian/giga/util/Utility.java | 3 +- 11 files changed, 608 insertions(+), 550 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index f27e7467e..8fef9a995 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -767,7 +767,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck 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 + // 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; } @@ -793,7 +794,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck if (secondaryStreamUrl == null) { urls = new String[]{selectedStream.getUrl()}; } else { - urls = new String[]{secondaryStreamUrl, selectedStream.getUrl()}; + urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl}; } DownloadManagerService.startMission(context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength); diff --git a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java index 590c3704c..5239c5bb7 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java @@ -1,158 +1,191 @@ -package us.shandian.giga.get; - -import android.support.annotation.NonNull; -import android.util.Log; - -import org.schabi.newpipe.streams.io.SharpStream; - -import java.io.IOException; -import java.io.InterruptedIOException; -import java.net.HttpURLConnection; -import java.nio.channels.ClosedByInterruptException; - -import us.shandian.giga.util.Utility; - -import static org.schabi.newpipe.BuildConfig.DEBUG; - -public class DownloadInitializer extends Thread { - private final static String TAG = "DownloadInitializer"; - final static int mId = 0; - - private DownloadMission mMission; - private HttpURLConnection mConn; - - DownloadInitializer(@NonNull DownloadMission mission) { - mMission = mission; - mConn = null; - } - - @Override - public void run() { - if (mMission.current > 0) mMission.resetState(false, true, DownloadMission.ERROR_NOTHING); - - int retryCount = 0; - while (true) { - try { - mMission.currentThreadCount = mMission.threadCount; - - mConn = mMission.openConnection(mId, -1, -1); - mMission.establishConnection(mId, mConn); - - if (!mMission.running || Thread.interrupted()) return; - - mMission.length = Utility.getContentLength(mConn); - - - if (mMission.length == 0) { - mMission.notifyError(DownloadMission.ERROR_HTTP_NO_CONTENT, null); - return; - } - - // check for dynamic generated content - if (mMission.length == -1 && mConn.getResponseCode() == 200) { - mMission.blocks = 0; - mMission.length = 0; - mMission.fallback = true; - mMission.unknownLength = true; - mMission.currentThreadCount = 1; - - if (DEBUG) { - Log.d(TAG, "falling back (unknown length)"); - } - } else { - // Open again - mConn = mMission.openConnection(mId, mMission.length - 10, mMission.length); - mMission.establishConnection(mId, mConn); - - if (!mMission.running || Thread.interrupted()) return; - - synchronized (mMission.blockState) { - if (mConn.getResponseCode() == 206) { - if (mMission.currentThreadCount > 1) { - mMission.blocks = mMission.length / DownloadMission.BLOCK_SIZE; - - if (mMission.currentThreadCount > mMission.blocks) { - mMission.currentThreadCount = (int) mMission.blocks; - } - if (mMission.currentThreadCount <= 0) { - mMission.currentThreadCount = 1; - } - if (mMission.blocks * DownloadMission.BLOCK_SIZE < mMission.length) { - mMission.blocks++; - } - } else { - // if one thread is solicited don't calculate blocks, is useless - mMission.blocks = 1; - mMission.fallback = true; - mMission.unknownLength = false; - } - - if (DEBUG) { - Log.d(TAG, "http response code = " + mConn.getResponseCode()); - } - } else { - // Fallback to single thread - mMission.blocks = 0; - mMission.fallback = true; - mMission.unknownLength = false; - mMission.currentThreadCount = 1; - - if (DEBUG) { - Log.d(TAG, "falling back due http response code = " + mConn.getResponseCode()); - } - } - - for (long i = 0; i < mMission.currentThreadCount; i++) { - mMission.threadBlockPositions.add(i); - mMission.threadBytePositions.add(0L); - } - } - - if (!mMission.running || Thread.interrupted()) return; - } - - SharpStream fs = mMission.storage.getStream(); - fs.setLength(mMission.offsets[mMission.current] + mMission.length); - fs.seek(mMission.offsets[mMission.current]); - fs.close(); - - if (!mMission.running || Thread.interrupted()) return; - - mMission.running = false; - break; - } catch (InterruptedIOException | ClosedByInterruptException e) { - return; - } catch (Exception e) { - if (!mMission.running) return; - - if (e instanceof IOException && e.getMessage().contains("Permission denied")) { - mMission.notifyError(DownloadMission.ERROR_PERMISSION_DENIED, e); - return; - } - - if (retryCount++ > mMission.maxRetry) { - Log.e(TAG, "initializer failed", e); - mMission.notifyError(e); - return; - } - - Log.e(TAG, "initializer failed, retrying", e); - } - } - - mMission.start(); - } - - @Override - public void interrupt() { - super.interrupt(); - - if (mConn != null) { - try { - mConn.disconnect(); - } catch (Exception e) { - // nothing to do - } - } - } -} +package us.shandian.giga.get; + +import android.support.annotation.NonNull; +import android.util.Log; + +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.HttpURLConnection; +import java.nio.channels.ClosedByInterruptException; + +import us.shandian.giga.util.Utility; + +import static org.schabi.newpipe.BuildConfig.DEBUG; + +public class DownloadInitializer extends Thread { + private final static String TAG = "DownloadInitializer"; + final static int mId = 0; + private final static int RESERVE_SPACE_DEFAULT = 5 * 1024 * 1024;// 5 MiB + private final static int RESERVE_SPACE_MAXIMUM = 150 * 1024 * 1024;// 150 MiB + + private DownloadMission mMission; + private HttpURLConnection mConn; + + DownloadInitializer(@NonNull DownloadMission mission) { + mMission = mission; + mConn = null; + } + + @Override + public void run() { + if (mMission.current > 0) mMission.resetState(false, true, DownloadMission.ERROR_NOTHING); + + int retryCount = 0; + while (true) { + try { + mMission.currentThreadCount = mMission.threadCount; + + if (mMission.blocks < 0 && mMission.current == 0) { + // calculate the whole size of the mission + long finalLength = 0; + long lowestSize = Long.MAX_VALUE; + + for (int i = 0; i < mMission.urls.length && mMission.running; i++) { + mConn = mMission.openConnection(mMission.urls[i], mId, -1, -1); + mMission.establishConnection(mId, mConn); + + if (Thread.interrupted()) return; + long length = Utility.getContentLength(mConn); + + if (i == 0) mMission.length = length; + if (length > 0) finalLength += length; + if (length < lowestSize) lowestSize = length; + } + + mMission.nearLength = finalLength; + + // reserve space at the start of the file + if (mMission.psAlgorithm != null && mMission.psAlgorithm.reserveSpace) { + if (lowestSize < 1) { + // the length is unknown use the default size + mMission.offsets[0] = RESERVE_SPACE_DEFAULT; + } else { + // use the smallest resource size to download, otherwise, use the maximum + mMission.offsets[0] = lowestSize < RESERVE_SPACE_MAXIMUM ? lowestSize : RESERVE_SPACE_MAXIMUM; + } + } + } else { + // ask for the current resource length + mConn = mMission.openConnection(mId, -1, -1); + mMission.establishConnection(mId, mConn); + + if (!mMission.running || Thread.interrupted()) return; + + mMission.length = Utility.getContentLength(mConn); + } + + if (mMission.length == 0 || mConn.getResponseCode() == 204) { + mMission.notifyError(DownloadMission.ERROR_HTTP_NO_CONTENT, null); + return; + } + + // check for dynamic generated content + if (mMission.length == -1 && mConn.getResponseCode() == 200) { + mMission.blocks = 0; + mMission.length = 0; + mMission.fallback = true; + mMission.unknownLength = true; + mMission.currentThreadCount = 1; + + if (DEBUG) { + Log.d(TAG, "falling back (unknown length)"); + } + } else { + // Open again + mConn = mMission.openConnection(mId, mMission.length - 10, mMission.length); + mMission.establishConnection(mId, mConn); + + if (!mMission.running || Thread.interrupted()) return; + + synchronized (mMission.blockState) { + if (mConn.getResponseCode() == 206) { + if (mMission.currentThreadCount > 1) { + mMission.blocks = mMission.length / DownloadMission.BLOCK_SIZE; + + if (mMission.currentThreadCount > mMission.blocks) { + mMission.currentThreadCount = (int) mMission.blocks; + } + if (mMission.currentThreadCount <= 0) { + mMission.currentThreadCount = 1; + } + if (mMission.blocks * DownloadMission.BLOCK_SIZE < mMission.length) { + mMission.blocks++; + } + } else { + // if one thread is solicited don't calculate blocks, is useless + mMission.blocks = 1; + mMission.fallback = true; + mMission.unknownLength = false; + } + + if (DEBUG) { + Log.d(TAG, "http response code = " + mConn.getResponseCode()); + } + } else { + // Fallback to single thread + mMission.blocks = 0; + mMission.fallback = true; + mMission.unknownLength = false; + mMission.currentThreadCount = 1; + + if (DEBUG) { + Log.d(TAG, "falling back due http response code = " + mConn.getResponseCode()); + } + } + + for (long i = 0; i < mMission.currentThreadCount; i++) { + mMission.threadBlockPositions.add(i); + mMission.threadBytePositions.add(0L); + } + } + + if (!mMission.running || Thread.interrupted()) return; + } + + SharpStream fs = mMission.storage.getStream(); + fs.setLength(mMission.offsets[mMission.current] + mMission.length); + fs.seek(mMission.offsets[mMission.current]); + fs.close(); + + if (!mMission.running || Thread.interrupted()) return; + + mMission.running = false; + break; + } catch (InterruptedIOException | ClosedByInterruptException e) { + return; + } catch (Exception e) { + if (!mMission.running) return; + + if (e instanceof IOException && e.getMessage().contains("Permission denied")) { + mMission.notifyError(DownloadMission.ERROR_PERMISSION_DENIED, e); + return; + } + + if (retryCount++ > mMission.maxRetry) { + Log.e(TAG, "initializer failed", e); + mMission.notifyError(e); + return; + } + + Log.e(TAG, "initializer failed, retrying", e); + } + } + + mMission.start(); + } + + @Override + public void interrupt() { + super.interrupt(); + + if (mConn != null) { + try { + mConn.disconnect(); + } catch (Exception e) { + // nothing to do + } + } + } +} diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMission.java b/app/src/main/java/us/shandian/giga/get/DownloadMission.java index 9bc46e3af..b3e32a43c 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -147,14 +147,10 @@ public class DownloadMission extends Mission { this.enqueued = true; this.maxRetry = 3; this.storage = storage; + this.psAlgorithm = psInstance; - if (psInstance != null) { - this.psAlgorithm = psInstance; - this.offsets[0] = psInstance.recommendedReserve; - } else { - if (DEBUG && urls.length > 1) { - Log.w(TAG, "mission created with multiple urls ¿missing post-processing algorithm?"); - } + if (DEBUG && psInstance == null && urls.length > 1) { + Log.w(TAG, "mission created with multiple urls ¿missing post-processing algorithm?"); } } @@ -233,10 +229,14 @@ public class DownloadMission extends Mission { * @throws IOException if an I/O exception occurs. */ HttpURLConnection openConnection(int threadId, long rangeStart, long rangeEnd) throws IOException { - URL url = new URL(urls[current]); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + return openConnection(urls[current], threadId, rangeStart, rangeEnd); + } + + HttpURLConnection openConnection(String url, int threadId, long rangeStart, long rangeEnd) throws IOException { + HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); 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); @@ -536,8 +536,11 @@ public class DownloadMission extends Mission { @Override public boolean delete() { deleted = true; + if (psAlgorithm != null) psAlgorithm.cleanupTemporalDir(); + boolean res = deleteThisFromFile(); - if (!super.delete()) res = false; + + if (!super.delete()) return false; return res; } @@ -626,6 +629,11 @@ public class DownloadMission extends Mission { return blocks >= 0; // DownloadMissionInitializer was executed } + /** + * Gets the approximated final length of the file + * + * @return the length in bytes + */ public long getLength() { long calculated; if (psState == 1 || psState == 3) { @@ -681,6 +689,8 @@ public class DownloadMission extends Mission { private boolean doPostprocessing() { if (psAlgorithm == null || psState == 2) return true; + errObject = null; + notifyPostProcessing(1); notifyProgress(0); diff --git a/app/src/main/java/us/shandian/giga/postprocessing/M4aNoDash.java b/app/src/main/java/us/shandian/giga/postprocessing/M4aNoDash.java index ee7e4cba1..aa5170908 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/M4aNoDash.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/M4aNoDash.java @@ -6,10 +6,10 @@ import org.schabi.newpipe.streams.io.SharpStream; import java.io.IOException; -public class M4aNoDash extends Postprocessing { +class M4aNoDash extends Postprocessing { M4aNoDash() { - super(0, true); + super(false, true, ALGORITHM_M4A_NO_DASH); } @Override diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java index f12c1c2d2..74cb43116 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java @@ -11,7 +11,7 @@ import java.io.IOException; class Mp4FromDashMuxer extends Postprocessing { Mp4FromDashMuxer() { - super(3 * 1024 * 1024/* 3 MiB */, true); + super(true, true, ALGORITHM_MP4_FROM_DASH_MUXER); } @Override diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java index 3d10628e7..15c4f575d 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java @@ -1,247 +1,256 @@ -package us.shandian.giga.postprocessing; - -import android.os.Message; -import android.support.annotation.NonNull; -import android.util.Log; - -import org.schabi.newpipe.streams.io.SharpStream; - -import java.io.File; -import java.io.IOException; -import java.io.Serializable; - -import us.shandian.giga.get.DownloadMission; -import us.shandian.giga.io.ChunkFileInputStream; -import us.shandian.giga.io.CircularFileWriter; -import us.shandian.giga.io.CircularFileWriter.OffsetChecker; -import us.shandian.giga.service.DownloadManagerService; - -import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING; -import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD; -import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; - -public abstract class Postprocessing implements Serializable { - - static transient final byte OK_RESULT = ERROR_NOTHING; - - public transient static final String ALGORITHM_TTML_CONVERTER = "ttml"; - public transient static final String ALGORITHM_WEBM_MUXER = "webm"; - public transient static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4"; - public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a"; - - public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args, @NonNull File cacheDir) { - Postprocessing instance; - - switch (algorithmName) { - case ALGORITHM_TTML_CONVERTER: - instance = new TtmlConverter(); - break; - case ALGORITHM_WEBM_MUXER: - instance = new WebMMuxer(); - break; - case ALGORITHM_MP4_FROM_DASH_MUXER: - instance = new Mp4FromDashMuxer(); - break; - case ALGORITHM_M4A_NO_DASH: - instance = new M4aNoDash(); - break; - /*case "example-algorithm": - instance = new ExampleAlgorithm();*/ - default: - throw new RuntimeException("Unimplemented post-processing algorithm: " + algorithmName); - } - - instance.args = args; - instance.name = algorithmName;// for debug only, maybe remove this field in the future - instance.cacheDir = cacheDir; - - return instance; - } - - /** - * Get a boolean value that indicate if the given algorithm work on the same - * file - */ - public boolean worksOnSameFile; - - /** - * Get the recommended space to reserve for the given algorithm. The amount - * is in bytes - */ - public int recommendedReserve; - - /** - * the download to post-process - */ - protected transient DownloadMission mission; - - public transient File cacheDir; - - private String[] args; - - private String name; - - Postprocessing(int recommendedReserve, boolean worksOnSameFile) { - this.recommendedReserve = recommendedReserve; - this.worksOnSameFile = worksOnSameFile; - } - - public void run(DownloadMission target) throws IOException { - this.mission = target; - - File temp = null; - CircularFileWriter out = null; - int result; - long finalLength = -1; - - mission.done = 0; - mission.length = mission.storage.length(); - - if (worksOnSameFile) { - ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length]; - try { - int i = 0; - for (; i < sources.length - 1; i++) { - sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i], mission.offsets[i + 1]); - } - sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i]); - - if (test(sources)) { - for (SharpStream source : sources) source.rewind(); - - OffsetChecker checker = () -> { - for (ChunkFileInputStream source : sources) { - /* - * WARNING: never use rewind() in any chunk after any writing (especially on first chunks) - * or the CircularFileWriter can lead to unexpected results - */ - if (source.isClosed() || source.available() < 1) { - continue;// the selected source is not used anymore - } - - return source.getFilePointer() - 1; - } - - return -1; - }; - - temp = new File(cacheDir, mission.storage.getName() + ".tmp"); - - out = new CircularFileWriter(mission.storage.getStream(), temp, checker); - out.onProgress = this::progressReport; - - out.onWriteError = (err) -> { - mission.psState = 3; - mission.notifyError(ERROR_POSTPROCESSING_HOLD, err); - - try { - synchronized (this) { - while (mission.psState == 3) - wait(); - } - } catch (InterruptedException e) { - // nothing to do - Log.e(this.getClass().getSimpleName(), "got InterruptedException"); - } - - return mission.errCode == ERROR_NOTHING; - }; - - result = process(out, sources); - - if (result == OK_RESULT) - finalLength = out.finalizeFile(); - } else { - result = OK_RESULT; - } - } finally { - for (SharpStream source : sources) { - if (source != null && !source.isClosed()) { - source.close(); - } - } - if (out != null) { - out.close(); - } - if (temp != null) { - //noinspection ResultOfMethodCallIgnored - temp.delete(); - } - } - } 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(); - } -} +package us.shandian.giga.postprocessing; + +import android.os.Message; +import android.support.annotation.NonNull; +import android.util.Log; + +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; + +import us.shandian.giga.get.DownloadMission; +import us.shandian.giga.io.ChunkFileInputStream; +import us.shandian.giga.io.CircularFileWriter; +import us.shandian.giga.io.CircularFileWriter.OffsetChecker; +import us.shandian.giga.service.DownloadManagerService; + +import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING; +import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD; +import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; + +public abstract class Postprocessing implements Serializable { + + static transient final byte OK_RESULT = ERROR_NOTHING; + + public transient static final String ALGORITHM_TTML_CONVERTER = "ttml"; + public transient static final String ALGORITHM_WEBM_MUXER = "webm"; + public transient static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4"; + public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a"; + + public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args) { + Postprocessing instance; + + switch (algorithmName) { + case ALGORITHM_TTML_CONVERTER: + instance = new TtmlConverter(); + break; + case ALGORITHM_WEBM_MUXER: + instance = new WebMMuxer(); + break; + case ALGORITHM_MP4_FROM_DASH_MUXER: + instance = new Mp4FromDashMuxer(); + break; + case ALGORITHM_M4A_NO_DASH: + instance = new M4aNoDash(); + break; + /*case "example-algorithm": + instance = new ExampleAlgorithm();*/ + default: + throw new UnsupportedOperationException("Unimplemented post-processing algorithm: " + algorithmName); + } + + instance.args = args; + return instance; + } + + /** + * Get a boolean value that indicate if the given algorithm work on the same + * file + */ + public final boolean worksOnSameFile; + + /** + * Indicates whether the selected algorithm needs space reserved at the beginning of the file + */ + public final boolean reserveSpace; + + /** + * Gets the given algorithm short name + */ + private final String name; + + + private String[] args; + + protected transient DownloadMission mission; + + private File tempFile; + + Postprocessing(boolean reserveSpace, boolean worksOnSameFile, String algorithmName) { + this.reserveSpace = reserveSpace; + this.worksOnSameFile = worksOnSameFile; + this.name = algorithmName;// for debugging only + } + + public void setTemporalDir(@NonNull File directory) { + long rnd = (int) (Math.random() * 100000f); + tempFile = new File(directory, rnd + "_" + System.nanoTime() + ".tmp"); + } + + public void cleanupTemporalDir() { + if (tempFile != null && tempFile.exists()) { + //noinspection ResultOfMethodCallIgnored + tempFile.delete(); + } + } + + + public void run(DownloadMission target) throws IOException { + this.mission = target; + + CircularFileWriter out = null; + int result; + long finalLength = -1; + + mission.done = 0; + mission.length = mission.storage.length(); + + if (worksOnSameFile) { + ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length]; + try { + int i = 0; + for (; i < sources.length - 1; i++) { + sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i], mission.offsets[i + 1]); + } + sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i]); + + if (test(sources)) { + for (SharpStream source : sources) source.rewind(); + + OffsetChecker checker = () -> { + for (ChunkFileInputStream source : sources) { + /* + * WARNING: never use rewind() in any chunk after any writing (especially on first chunks) + * or the CircularFileWriter can lead to unexpected results + */ + if (source.isClosed() || source.available() < 1) { + continue;// the selected source is not used anymore + } + + return source.getFilePointer() - 1; + } + + return -1; + }; + + out = new CircularFileWriter(mission.storage.getStream(), tempFile, checker); + out.onProgress = this::progressReport; + + out.onWriteError = (err) -> { + mission.psState = 3; + mission.notifyError(ERROR_POSTPROCESSING_HOLD, err); + + try { + synchronized (this) { + while (mission.psState == 3) + wait(); + } + } catch (InterruptedException e) { + // nothing to do + Log.e(this.getClass().getSimpleName(), "got InterruptedException"); + } + + return mission.errCode == ERROR_NOTHING; + }; + + result = process(out, sources); + + if (result == OK_RESULT) + finalLength = out.finalizeFile(); + } 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(); + } +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java b/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java index bba0b299a..5a5b687f7 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java @@ -1,72 +1,72 @@ -package us.shandian.giga.postprocessing; - -import android.util.Log; - -import org.schabi.newpipe.streams.SubtitleConverter; -import org.schabi.newpipe.streams.io.SharpStream; -import org.xml.sax.SAXException; - -import java.io.IOException; -import java.text.ParseException; - -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.xpath.XPathExpressionException; - -/** - * @author kapodamy - */ -class TtmlConverter extends Postprocessing { - private static final String TAG = "TtmlConverter"; - - TtmlConverter() { - // due how XmlPullParser works, the xml is fully loaded on the ram - super(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 - String format = getArgumentAt(0, null); - - if (format == null || format.equals("ttml")) { - SubtitleConverter ttmlDumper = new SubtitleConverter(); - - try { - ttmlDumper.dumpTTML( - sources[0], - out, - getArgumentAt(1, "true").equals("true"), - getArgumentAt(2, "true").equals("true") - ); - } catch (Exception err) { - Log.e(TAG, "subtitle parse failed", err); - - if (err instanceof IOException) { - return 1; - } else if (err instanceof ParseException) { - return 2; - } else if (err instanceof SAXException) { - return 3; - } else if (err instanceof ParserConfigurationException) { - return 4; - } else if (err instanceof XPathExpressionException) { - return 7; - } - - return 8; - } - - return OK_RESULT; - } else if (format.equals("srt")) { - byte[] buffer = new byte[8 * 1024]; - int read; - while ((read = sources[0].read(buffer)) > 0) { - out.write(buffer, 0, read); - } - return OK_RESULT; - } - - throw new UnsupportedOperationException("Can't convert this subtitle, unimplemented format: " + format); - } - -} +package us.shandian.giga.postprocessing; + +import android.util.Log; + +import org.schabi.newpipe.streams.SubtitleConverter; +import org.schabi.newpipe.streams.io.SharpStream; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.text.ParseException; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPathExpressionException; + +/** + * @author kapodamy + */ +class TtmlConverter extends Postprocessing { + private static final String TAG = "TtmlConverter"; + + TtmlConverter() { + // due how XmlPullParser works, the xml is fully loaded on the ram + super(false, true, ALGORITHM_TTML_CONVERTER); + } + + @Override + int process(SharpStream out, SharpStream... sources) throws IOException { + // check if the subtitle is already in srt and copy, this should never happen + String format = getArgumentAt(0, null); + + if (format == null || format.equals("ttml")) { + SubtitleConverter ttmlDumper = new SubtitleConverter(); + + try { + ttmlDumper.dumpTTML( + sources[0], + out, + getArgumentAt(1, "true").equals("true"), + getArgumentAt(2, "true").equals("true") + ); + } catch (Exception err) { + Log.e(TAG, "subtitle parse failed", err); + + if (err instanceof IOException) { + return 1; + } else if (err instanceof ParseException) { + return 2; + } else if (err instanceof SAXException) { + return 3; + } else if (err instanceof ParserConfigurationException) { + return 4; + } else if (err instanceof XPathExpressionException) { + return 7; + } + + return 8; + } + + return OK_RESULT; + } else if (format.equals("srt")) { + byte[] buffer = new byte[8 * 1024]; + int read; + while ((read = sources[0].read(buffer)) > 0) { + out.write(buffer, 0, read); + } + return OK_RESULT; + } + + throw new UnsupportedOperationException("Can't convert this subtitle, unimplemented format: " + format); + } + +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java index 618c1ec5a..ea1676482 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java @@ -1,44 +1,44 @@ -package us.shandian.giga.postprocessing; - -import org.schabi.newpipe.streams.WebMReader.TrackKind; -import org.schabi.newpipe.streams.WebMReader.WebMTrack; -import org.schabi.newpipe.streams.WebMWriter; -import org.schabi.newpipe.streams.io.SharpStream; - -import java.io.IOException; - -/** - * @author kapodamy - */ -class WebMMuxer extends Postprocessing { - - WebMMuxer() { - super(5 * 1024 * 1024/* 5 MiB */, true); - } - - @Override - int process(SharpStream out, SharpStream... sources) throws IOException { - WebMWriter muxer = new WebMWriter(sources); - muxer.parseSources(); - - // youtube uses a webm with a fake video track that acts as a "cover image" - int[] indexes = new int[sources.length]; - - for (int i = 0; i < sources.length; i++) { - WebMTrack[] tracks = muxer.getTracksFromSource(i); - for (int j = 0; j < tracks.length; j++) { - if (tracks[j].kind == TrackKind.Audio) { - indexes[i] = j; - i = sources.length; - break; - } - } - } - - muxer.selectTracks(indexes); - muxer.build(out); - - return OK_RESULT; - } - -} +package us.shandian.giga.postprocessing; + +import org.schabi.newpipe.streams.WebMReader.TrackKind; +import org.schabi.newpipe.streams.WebMReader.WebMTrack; +import org.schabi.newpipe.streams.WebMWriter; +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.IOException; + +/** + * @author kapodamy + */ +class WebMMuxer extends Postprocessing { + + WebMMuxer() { + super(true, true, ALGORITHM_WEBM_MUXER); + } + + @Override + int process(SharpStream out, SharpStream... sources) throws IOException { + WebMWriter muxer = new WebMWriter(sources); + muxer.parseSources(); + + // youtube uses a webm with a fake video track that acts as a "cover image" + int[] indexes = new int[sources.length]; + + for (int i = 0; i < sources.length; i++) { + WebMTrack[] tracks = muxer.getTracksFromSource(i); + for (int j = 0; j < tracks.length; j++) { + if (tracks[j].kind == TrackKind.Audio) { + indexes[i] = j; + i = sources.length; + break; + } + } + } + + muxer.selectTracks(indexes); + muxer.build(out); + + return OK_RESULT; + } + +} diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java index 479c4b92f..c2bba7396 100644 --- a/app/src/main/java/us/shandian/giga/service/DownloadManager.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java @@ -154,7 +154,9 @@ public class DownloadManager { if (mis.psAlgorithm.worksOnSameFile) { // Incomplete post-processing results in a corrupted download file // because the selected algorithm works on the same file to save space. - if (exists && !mis.storage.delete()) + // 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; @@ -162,7 +164,6 @@ public class DownloadManager { mis.psState = 0; mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED; - mis.errObject = null; } else if (!exists) { tryRecover(mis); @@ -171,8 +172,10 @@ public class DownloadManager { mis.resetState(true, true, DownloadMission.ERROR_PROGRESS_LOST); } - if (mis.psAlgorithm != null) - mis.psAlgorithm.cacheDir = pickAvailableCacheDir(ctx); + if (mis.psAlgorithm != null) { + mis.psAlgorithm.cleanupTemporalDir(); + mis.psAlgorithm.setTemporalDir(pickAvailableTemporalDir(ctx)); + } mis.recovered = exists; mis.metadata = sub; @@ -532,14 +535,14 @@ public class DownloadManager { } private static boolean isDirectoryAvailable(File directory) { - return directory != null && directory.canWrite(); + return directory != null && directory.canWrite() && directory.exists(); } - static File pickAvailableCacheDir(@NonNull Context ctx) { - if (isDirectoryAvailable(ctx.getExternalCacheDir())) - return ctx.getExternalCacheDir(); - else if (isDirectoryAvailable(ctx.getCacheDir())) - return ctx.getCacheDir(); + 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); @@ -550,7 +553,7 @@ public class DownloadManager { if (tag.equals(TAG_AUDIO)) return mMainStorageAudio; if (tag.equals(TAG_VIDEO)) return mMainStorageVideo; - Log.w(TAG, "Unknown download category, not [audio video]: " + String.valueOf(tag)); + Log.w(TAG, "Unknown download category, not [audio video]: " + tag); return null;// this never should happen } diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index f25147507..aab0257db 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -450,13 +450,16 @@ public class DownloadManagerService extends Service { if (psName == null) ps = null; else - ps = Postprocessing.getAlgorithm(psName, psArgs, DownloadManager.pickAvailableCacheDir(this)); + 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); diff --git a/app/src/main/java/us/shandian/giga/util/Utility.java b/app/src/main/java/us/shandian/giga/util/Utility.java index 793cbea18..dc6a67b4b 100644 --- a/app/src/main/java/us/shandian/giga/util/Utility.java +++ b/app/src/main/java/us/shandian/giga/util/Utility.java @@ -267,8 +267,7 @@ public class Utility { } try { - long length = Long.parseLong(connection.getHeaderField("Content-Length")); - if (length >= 0) return length; + return Long.parseLong(connection.getHeaderField("Content-Length")); } catch (Exception err) { // nothing to do } From cdc8fe86cee3f344f4a54d2fdca1f6f296e62c27 Mon Sep 17 00:00:00 2001 From: kapodamy Date: Mon, 3 Jun 2019 18:39:56 -0300 Subject: [PATCH 21/30] amend rebase resolve inconsistency in string.xml files --- app/src/main/res/values-ca/strings.xml | 3 ++- app/src/main/res/values-cs/strings.xml | 3 ++- app/src/main/res/values-da/strings.xml | 6 +++--- app/src/main/res/values-de/strings.xml | 4 ++-- app/src/main/res/values-el/strings.xml | 3 ++- app/src/main/res/values-es/strings.xml | 23 +++++++++-------------- app/src/main/res/values-et/strings.xml | 3 ++- app/src/main/res/values-fr/strings.xml | 3 ++- app/src/main/res/values-hr/strings.xml | 3 ++- app/src/main/res/values-ja/strings.xml | 4 ++-- app/src/main/res/values-ko/strings.xml | 3 ++- app/src/main/res/values-ru/strings.xml | 1 - app/src/main/res/values-sk/strings.xml | 3 ++- app/src/main/res/values-uk/strings.xml | 3 ++- 14 files changed, 34 insertions(+), 31 deletions(-) diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 04bb36ea3..e631c4fe0 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -446,7 +446,8 @@ Ha fallat la baixada Baixada finalitzada %s baixades finalitzades - Ja existeix un fitxer baixat amb aquest nom + Ja existeix un fitxer baixat amb aquest nom + Ja existeix un fitxer amb aquest nom Hi ha una baixada en curs amb aquest nom Ha fallat la connexió segura No s\'ha pogut trobar el servidor diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 306fb4ccd..97326bfdc 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -459,7 +459,8 @@ otevření ve vyskakovacím okně % s stahování dokončeno Vytvořit jedinečný název Přepsat - Stažený soubor s tímto názvem již existuje + Stažený soubor s tímto názvem již existuje + Stažený soubor s tímto názvem již existuje Stahování s tímto názvem již probíhá Zobrazit chybu Kód diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 0c699cf0e..919cef47b 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -359,7 +359,7 @@ Automatisk Tryk for at downloade Færdig - I kø + Afventning efterbehandling Handling afvist af systemet @@ -368,7 +368,8 @@ %s downloads færdige Generer unikt navn Overskriv - En downloadet fil med dette navn eksisterer allerede + En fil med dette navn eksisterer allerede + En downloadet fil med dette navn eksisterer allerede Der er en download i gang med dette navn Vis fejl Kode @@ -453,5 +454,4 @@ Maksimalt antal forsøg før downloaden opgives Sæt på pause ved skift til mobildata Downloads som ikke kan sættes på pause vil blive genstartet - Afventning \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 9a2518652..7c131eac4 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -448,8 +448,8 @@ %s heruntergeladen Eindeutigen Namen erzeugen Überschreiben - Eine heruntergeladene Datei dieses Namens existiert bereits - Eine Datei dieses Namens wird gerade heruntergeladen + Eine Datei mit diesem Namen existiert bereits + Eine heruntergeladene Datei mit diesem Namen existiert bereits Fehler anzeigen Code Die Datei kann nicht erstellt werden diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 68e93ba58..aa87c9c32 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -444,7 +444,8 @@ %s λήψεις ολοκρηρώθηκαν Δημιουργία μοναδικού ονόματος Αντικατάσταση - Ένα αρχείο με το ίδιο όνομα υπάρχει ήδη + Ένα αρχείο με αυτό το όνομα υπάρχει ήδη + Ένα αρχείο που έχει ληφθεί με αυτό το όνομα υπάρχει ήδη Υπάρχει μια λήψη σε εξέλιξη με αυτό το όνομα Εμφάνιση σφάλματος Κωδικός diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 890755845..8f4375033 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -159,7 +159,7 @@ abrir en modo popup Si tienes ideas de; traducción, cambios de diseño, limpieza de código o cambios de código realmente fuertes—la ayuda siempre es bienvenida. Cuanto más se hace, mejor se pone! Leer licencia Contribuir -Suscribirse + Suscribirse Suscrito Canal no suscrito No se pudo cambiar la suscripción @@ -211,8 +211,8 @@ abrir en modo popup Vídeos Elemento eliminado -¿Desea eliminar este elemento del historial de búsqueda? -Contenido de la página principal + ¿Desea eliminar este elemento del historial de búsqueda? + Contenido de la página principal Página en blanco Página del kiosco Página de suscripción @@ -224,7 +224,7 @@ abrir en modo popup Kiosco Tendencias Top 50 -Mostrar sugerencia cuando se presiona el botón de segundo plano o popup en la página de detalles del vídeo + Mostrar sugerencia cuando se presiona el botón de segundo plano o popup en la página de detalles del vídeo En cola en el reproductor de fondo En cola en el reproductor popup Reproducir todo @@ -242,7 +242,7 @@ abrir en modo popup Comenzar a reproducir aquí Comenzar aquí en segundo plano Comenzar aquí en popup -Mostrar consejo \"Mantener para poner en la cola\" + Mostrar consejo \"Mantener para poner en la cola\" Nuevo y popular Mantener para poner en la cola Donar @@ -270,7 +270,7 @@ abrir en modo popup Reproductor de popup Obteniendo información… Cargando contenido solicitado -Importar base de datos + Importar base de datos Exportar base de datos Reemplazará su historial actual y sus suscripciones Exportar historial, suscripciones y listas de reproducción @@ -404,7 +404,7 @@ abrir en modo popup Listas de reproducción Pistas Finalizadas - En cola + Pendientes pausado en cola post-procesado @@ -423,12 +423,11 @@ abrir en modo popup No se puede sobrescribir el archivo Hay una descarga en curso con este nombre Hay una descarga pendiente con este nombre - Mostrar como grilla Mostrar como lista Limpiar descargas finalizadas - ¿Estas seguro? Tienes %s descargas pendientes, ve a Descargas para continuarlas + ¿Estas seguro? Detener Intentos máximos Cantidad máxima de intentos antes de cancelar la descarga @@ -459,11 +458,7 @@ abrir en modo popup Se perdió el progreso porque el archivo fue eliminado Tiempo de espera excedido - No es posible descargar a una tarjeta SD externa. \¿Restablecer la ubicación de la carpeta de descarga\? - Seleccione los directorios de descarga - Pendiente - Preguntar dónde descargar Se preguntará dónde guardar cada descarga Se preguntará dónde guardar cada descarga.\nHabilita esta opción si quieres descargar en la tarjeta SD externa @@ -480,7 +475,7 @@ abrir en modo popup Notificación de actualización de la aplicación Notificaciones para nueva versión de NewPipe Almacenamiento externo no disponible - Todavía no es posible descargar a una tarjeta SD externa. ¿Restablecer la ubicación de la carpeta de descarga\? + No es posible descargar a una tarjeta SD externa. \¿Restablecer la ubicación de la carpeta de descarga\? Usando las pestañas por defecto, error al leer las pestañas guardadas Restaurar valores por defecto ¿Quieres restaurar los valores por defecto\? diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index a11805f3b..35e2a7695 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -445,7 +445,8 @@ %s allalaadimist lõppenud Loo kordumatu nimi Kirjuta üle - Selle nimega allalaetud fail on juba olemas + Sellise nimega fail on juba olemas + Selle nimega allalaaditud fail on juba olemas Selle nimega allalaadimine on käimas Näita viga Kood diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index ca9682fdb..301d41d36 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -447,7 +447,8 @@ Ajouté à la file d\'attente Générer un nom unique Écraser - Un fichier téléchargé avec ce nom existe déjà + Un fichier avec ce nom existe déjà + Un fichier téléchargé avec ce nom existe déjà Il y a un téléchargement en cours avec ce nom Afficher l\'erreur Code diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index e7aeb924a..3d7601895 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -447,7 +447,8 @@ %s preuzimanja dovršeno Generirajte jedinstveni naziv Piši preko - Preuzeta datoteka s tim nazivom već postoji + Datoteka s tim nazivom već postoji + Preuzeta datoteka s tim nazivom već postoji U tijeku je preuzimanje s ovim nazivom Kod Datoteku nije moguće izraditi diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index be243bffd..ba0e7b1ba 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -432,7 +432,8 @@ %s 件のダウンロード終了 一意の名前を生成します 上書き - 同じ名前のファイルが既に存在します + この名前のファイルは既に存在します + この名前のダウンロードファイルは既に存在します 同じ名前を持つダウンロードが既に進行中です エラーを表示する コード @@ -464,6 +465,5 @@ ダウンロードから %s の保留中の転送を続行します モバイルデータ通信に切り替え時に休止 休止できないダウンロードが再開されます - 保留中 接続タイムアウト \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 1bb71650d..c02b7270e 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -450,7 +450,8 @@ %s 다운로드 완료됨 별개의 이름 생성 덮어쓰기 - 해당 이름을 가진 다운로드된 파일이 이미 존재합니다 + 이 이름을 가진 파일이 이미 있습니다. + 이 이름을 가진 다운로드 된 파일이 이미 있습니다. 해당 이름을 가진 다운로드가 이미 진행중입니다 오류 표시 코드 diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index c75af14e2..b5e3769ad 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -472,6 +472,5 @@ Пост-обработка не удалась Останавливать скачивание при переходе на мобильную сеть Закрыть - в ожидании Время соединения вышло \ No newline at end of file diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index de26a1638..dc53835e4 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -456,7 +456,8 @@ %s sťahovania skončené Vytvoriť jedinečný názov Prepísať - Stiahnutý súbor s týmto menom už existuje + Súbor s týmto názvom už existuje + Stiahnutý súbor s týmto názvom už existuje Sťahovanie s týmto názvom už prebieha Zobraziť chybu Kód diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 60934ebb1..1577371ef 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -443,7 +443,8 @@ %s завантажень завершено Згенерувати унікальну назву Перезаписати - Завантажений файл з таким ім\'ям вже існує + Файл з такою назвою вже існує + Завантажений файл з такою назвою вже існує Файл з такою назвою вже завантажується Показати помилку Код From ac5e2e0532abe876bedee8984235a92a29345053 Mon Sep 17 00:00:00 2001 From: kapodamy Date: Fri, 14 Jun 2019 12:19:50 -0300 Subject: [PATCH 22/30] bugs fixes * fix storage warning dialogs created on invalid contexts * implement mkdirs in StoredDirectoryHelper --- .../newpipe/download/DownloadDialog.java | 33 +++++++++++++ .../fragments/detail/VideoDetailFragment.java | 2 +- .../giga/io/StoredDirectoryHelper.java | 46 +++++++++++++++++++ .../giga/service/DownloadManagerService.java | 27 ----------- 4 files changed, 80 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 8fef9a995..56ea9366d 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -217,6 +217,32 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck 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 @@ -520,6 +546,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck private void showFailedDialog(@StringRes int msg) { new AlertDialog.Builder(context) + .setTitle(R.string.general_error) .setMessage(msg) .setNegativeButton(android.R.string.ok, null) .create() @@ -631,6 +658,12 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck // 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); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index bbd1a315d..c89e773f4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -1195,7 +1195,7 @@ public class VideoDetailFragment downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex); downloadDialog.setSubtitleStreams(currentInfo.getSubtitles()); - downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); + downloadDialog.show(getActivity().getSupportFragmentManager(), "downloadDialog"); } catch (Exception e) { ErrorActivity.ErrorInfo info = ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, ServiceList.all() diff --git a/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java b/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java index a65c4dff3..aeb810479 100644 --- a/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java +++ b/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java @@ -144,6 +144,52 @@ public class StoredDirectoryHelper { 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 true if and only if the directory was created, + * along with all necessary parent directories or already exists; false + * 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; } diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index aab0257db..7f3a4bde1 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -270,33 +270,6 @@ public class DownloadManagerService extends Service { Toast.makeText(this, "Permission denied (write)", Toast.LENGTH_SHORT).show(); } - // Check download save paths - - String msg = ""; - if (mManager.mMainStorageVideo == null) - msg += getString(R.string.download_path_title); - else if (mManager.mMainStorageAudio == null) - msg += getString(R.string.download_path_audio_title); - - if (!msg.isEmpty()) { - String title; - if (mManager.mMainStorageVideo == null && mManager.mMainStorageAudio == 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(this) - .setPositiveButton(android.R.string.ok, null) - .setTitle(title) - .setMessage(msg) - .create() - .show(); - } - - return mBinder; } From e599de038a6ae3dbcd0dae6e6b9433efc459559a Mon Sep 17 00:00:00 2001 From: Redirion Date: Tue, 25 Jun 2019 11:49:59 +0200 Subject: [PATCH 23/30] Silence CheckForNewAppVersionTask Closes #2421 --- .../java/org/schabi/newpipe/CheckForNewAppVersionTask.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index 6a6d1b9c2..9285820e8 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -90,9 +90,7 @@ public class CheckForNewAppVersionTask extends AsyncTask { Response response = client.newCall(request).execute(); return response.body().string(); } catch (IOException ex) { - ErrorActivity.reportError(app, ex, null, null, - ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", - "app update API fail", R.string.app_ui_crash)); + // connectivity problems, do not alarm user and fail silently } return null; From 00074517350d13fff3108ba4d2683dc92fa9f3e4 Mon Sep 17 00:00:00 2001 From: Redirion Date: Tue, 25 Jun 2019 13:22:40 +0200 Subject: [PATCH 24/30] Update CheckForNewAppVersionTask.java --- .../java/org/schabi/newpipe/CheckForNewAppVersionTask.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index 9285820e8..08c4ca90a 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -47,6 +47,7 @@ import okhttp3.Response; */ public class CheckForNewAppVersionTask extends AsyncTask { + private static final boolean DEBUG = MainActivity.DEBUG; private static final Application app = App.getApp(); private static final String GITHUB_APK_SHA1 = "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15"; private static final String newPipeApiUrl = "https://newpipe.schabi.org/api/data.json"; @@ -91,6 +92,7 @@ public class CheckForNewAppVersionTask extends AsyncTask { return response.body().string(); } catch (IOException ex) { // connectivity problems, do not alarm user and fail silently + if (DEBUG) ex.printStackTrace(); } return null; @@ -115,9 +117,8 @@ public class CheckForNewAppVersionTask extends AsyncTask { compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode); } catch (JSONException ex) { - ErrorActivity.reportError(app, ex, null, null, - ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none", - "could not parse app update JSON data", R.string.app_ui_crash)); + // connectivity problems, do not alarm user and fail silently + if (DEBUG) ex.printStackTrace(); } } } From 05ef926a7f6bf046c19e945fa2d6e72227a2c671 Mon Sep 17 00:00:00 2001 From: Redirion Date: Tue, 25 Jun 2019 13:31:26 +0200 Subject: [PATCH 25/30] Update CheckForNewAppVersionTask.java --- .../java/org/schabi/newpipe/CheckForNewAppVersionTask.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index 08c4ca90a..642cf8910 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -48,6 +48,7 @@ import okhttp3.Response; public class CheckForNewAppVersionTask extends AsyncTask { private static final boolean DEBUG = MainActivity.DEBUG; + private static final String TAG = CheckForNewAppVersionTask.class.getSimpleName(); private static final Application app = App.getApp(); private static final String GITHUB_APK_SHA1 = "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15"; private static final String newPipeApiUrl = "https://newpipe.schabi.org/api/data.json"; @@ -92,7 +93,7 @@ public class CheckForNewAppVersionTask extends AsyncTask { return response.body().string(); } catch (IOException ex) { // connectivity problems, do not alarm user and fail silently - if (DEBUG) ex.printStackTrace(); + if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex)); } return null; @@ -118,7 +119,7 @@ public class CheckForNewAppVersionTask extends AsyncTask { } catch (JSONException ex) { // connectivity problems, do not alarm user and fail silently - if (DEBUG) ex.printStackTrace(); + if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex)); } } } From 6a4bb6e3e106e3f7c6d336bfa402b39274d8411f Mon Sep 17 00:00:00 2001 From: Redirion Date: Tue, 25 Jun 2019 13:39:47 +0200 Subject: [PATCH 26/30] Update CheckForNewAppVersionTask.java --- .../main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index 642cf8910..09c44cad4 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -94,6 +94,7 @@ public class CheckForNewAppVersionTask extends AsyncTask { } catch (IOException ex) { // connectivity problems, do not alarm user and fail silently if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex)); + Toast.makeText(app, R.string.error_connect_host, Toast.LENGTH_SHORT).show(); } return null; @@ -120,6 +121,7 @@ public class CheckForNewAppVersionTask extends AsyncTask { } catch (JSONException ex) { // connectivity problems, do not alarm user and fail silently if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex)); + Toast.makeText(app, R.string.error_connect_host, Toast.LENGTH_SHORT).show(); } } } From c4ef40f4dc65ca26c5b3fb99283b4839ab52db3d Mon Sep 17 00:00:00 2001 From: Redirion Date: Tue, 25 Jun 2019 13:41:08 +0200 Subject: [PATCH 27/30] Removed tabs --- .../org/schabi/newpipe/CheckForNewAppVersionTask.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index 09c44cad4..12bf0d778 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -93,8 +93,8 @@ public class CheckForNewAppVersionTask extends AsyncTask { return response.body().string(); } catch (IOException ex) { // connectivity problems, do not alarm user and fail silently - if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex)); - Toast.makeText(app, R.string.error_connect_host, Toast.LENGTH_SHORT).show(); + if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex)); + Toast.makeText(app, R.string.error_connect_host, Toast.LENGTH_SHORT).show(); } return null; @@ -120,8 +120,8 @@ public class CheckForNewAppVersionTask extends AsyncTask { } catch (JSONException ex) { // connectivity problems, do not alarm user and fail silently - if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex)); - Toast.makeText(app, R.string.error_connect_host, Toast.LENGTH_SHORT).show(); + if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex)); + Toast.makeText(app, R.string.error_connect_host, Toast.LENGTH_SHORT).show(); } } } From 80b49751887d429b179d101df452950751ec7a9b Mon Sep 17 00:00:00 2001 From: Redirion Date: Tue, 25 Jun 2019 13:47:16 +0200 Subject: [PATCH 28/30] Update CheckForNewAppVersionTask.java --- .../main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index 12bf0d778..bcc6525f9 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -14,6 +14,7 @@ import android.os.AsyncTask; import android.preference.PreferenceManager; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationManagerCompat; +import android.util.Log; import org.json.JSONException; import org.json.JSONObject; From 37a9e98ebc8bd433217d0d92b7bcef0ce1c57431 Mon Sep 17 00:00:00 2001 From: Redirion Date: Tue, 25 Jun 2019 13:53:23 +0200 Subject: [PATCH 29/30] Update CheckForNewAppVersionTask.java --- .../main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index bcc6525f9..13d399a1c 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -15,6 +15,7 @@ import android.preference.PreferenceManager; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationManagerCompat; import android.util.Log; +import android.widget.Toast; import org.json.JSONException; import org.json.JSONObject; From fc8746e077ded1bd5ae08602fb5816ffbbc09edf Mon Sep 17 00:00:00 2001 From: Redirion Date: Wed, 26 Jun 2019 18:37:04 +0200 Subject: [PATCH 30/30] Update CheckForNewAppVersionTask.java --- .../java/org/schabi/newpipe/CheckForNewAppVersionTask.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index 13d399a1c..87ffcb05d 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -15,7 +15,6 @@ import android.preference.PreferenceManager; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationManagerCompat; import android.util.Log; -import android.widget.Toast; import org.json.JSONException; import org.json.JSONObject; @@ -96,7 +95,6 @@ public class CheckForNewAppVersionTask extends AsyncTask { } catch (IOException ex) { // connectivity problems, do not alarm user and fail silently if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex)); - Toast.makeText(app, R.string.error_connect_host, Toast.LENGTH_SHORT).show(); } return null; @@ -123,7 +121,6 @@ public class CheckForNewAppVersionTask extends AsyncTask { } catch (JSONException ex) { // connectivity problems, do not alarm user and fail silently if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex)); - Toast.makeText(app, R.string.error_connect_host, Toast.LENGTH_SHORT).show(); } } }