From 20a8d7372c545accd8bfded39936259b8bddffab Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Sun, 26 Mar 2017 17:29:16 +0200 Subject: [PATCH 1/7] fix broken overlay on .svg icon --- assets/new_pipe_icon_5.svg | 40 ++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/assets/new_pipe_icon_5.svg b/assets/new_pipe_icon_5.svg index a1e73ba7f..c4b21bbd8 100644 --- a/assets/new_pipe_icon_5.svg +++ b/assets/new_pipe_icon_5.svg @@ -11,9 +11,9 @@ version="1.1" id="svg2" viewBox="0 0 192 192" - height="192" - width="192" - inkscape:version="0.91 r13725" + height="204.8" + width="204.8" + inkscape:version="0.92.1 r" sodipodi:docname="new_pipe_icon_5.svg" inkscape:export-filename="/home/the-scrabi/Projects/NewPipe/assets/new_pipe_icon_5.png" inkscape:export-xdpi="120" @@ -31,9 +31,9 @@ inkscape:window-height="1012" id="namedview4149" showgrid="false" - inkscape:zoom="2.2262658" - inkscape:cx="115.37498" - inkscape:cy="51.93678" + inkscape:zoom="1.2836556" + inkscape:cx="361.53411" + inkscape:cy="-71.516938" inkscape:window-x="0" inkscape:window-y="32" inkscape:window-maximized="1" @@ -89,7 +89,7 @@ id="feComposite4464" /> + gradientTransform="matrix(0.00132321,2.1587518,-2.1815784,0.00133718,1.1369842,-0.39215567)" /> @@ -472,7 +472,9 @@ id="path4144-9" r="88" cy="104" - cx="88" /> + cx="88" + d="" + inkscape:connector-curvature="0" /> From b43c56085d2fa0993fa844b899f44c36b8cf0810 Mon Sep 17 00:00:00 2001 From: Mauricio Colli Date: Mon, 27 Mar 2017 01:08:16 -0300 Subject: [PATCH 2/7] Implement fullscreen and quality selector - Implement cache - Abstract player - Quality selector - Fullscreen switcher - Change some drawables --- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 16 +- .../detail/VideoItemDetailFragment.java | 74 +- .../schabi/newpipe/player/AbstractPlayer.java | 1106 +++++++++++++++++ .../newpipe/player/BackgroundPlayer.java | 5 +- .../newpipe/player/ExoPlayerActivity.java | 621 ++++++--- .../newpipe/player/PlayVideoActivity.java | 4 +- .../newpipe/player/PopupVideoPlayer.java | 815 +++++------- .../player/{popup => }/StateInterface.java | 6 +- .../newpipe/player/popup/PopupViewHolder.java | 93 -- .../SearchInfoItemFragment.java | 7 +- .../ic_close_white.png} | Bin .../res/drawable-hdpi/ic_close_white_24dp.png | Bin 221 -> 0 bytes .../ic_fullscreen_exit_white.png | Bin 0 -> 105 bytes .../res/drawable-hdpi/ic_fullscreen_white.png | Bin 0 -> 107 bytes .../ic_pause_white.png} | Bin .../res/drawable-hdpi/ic_pause_white_24dp.png | Bin 105 -> 0 bytes ...white_48dp.png => ic_play_arrow_white.png} | Bin .../res/drawable-hdpi/ic_repeat_white.png | Bin 0 -> 234 bytes .../ic_close_white.png} | Bin .../res/drawable-mdpi/ic_close_white_24dp.png | Bin 175 -> 0 bytes .../ic_fullscreen_exit_white.png | Bin 0 -> 101 bytes .../res/drawable-mdpi/ic_fullscreen_white.png | Bin 0 -> 101 bytes .../ic_pause_white.png} | Bin .../res/drawable-mdpi/ic_pause_white_24dp.png | Bin 83 -> 0 bytes ...white_48dp.png => ic_play_arrow_white.png} | Bin .../res/drawable-mdpi/ic_repeat_white.png | Bin 0 -> 185 bytes .../ic_close_white.png} | Bin .../ic_fullscreen_exit_white.png | Bin 0 -> 106 bytes .../drawable-xhdpi/ic_fullscreen_white.png | Bin 0 -> 109 bytes .../ic_pause_white.png} | Bin ...white_48dp.png => ic_play_arrow_white.png} | Bin .../res/drawable-xhdpi/ic_repeat_white.png | Bin 0 -> 258 bytes .../res/drawable-xxhdpi/ic_close_white.png | Bin 0 -> 524 bytes .../ic_fullscreen_exit_white.png | Bin 0 -> 123 bytes .../drawable-xxhdpi/ic_fullscreen_white.png | Bin 0 -> 123 bytes .../res/drawable-xxhdpi/ic_pause_white.png | Bin 0 -> 110 bytes ...white_48dp.png => ic_play_arrow_white.png} | Bin .../res/drawable-xxhdpi/ic_repeat_white.png | Bin 0 -> 356 bytes .../res/drawable-xxxhdpi/ic_close_white.png | Bin 0 -> 707 bytes .../ic_fullscreen_exit_white.png | Bin 0 -> 125 bytes .../drawable-xxxhdpi/ic_fullscreen_white.png | Bin 0 -> 124 bytes .../res/drawable-xxxhdpi/ic_pause_white.png | Bin 0 -> 111 bytes ...white_48dp.png => ic_play_arrow_white.png} | Bin .../res/drawable-xxxhdpi/ic_repeat_white.png | Bin 0 -> 403 bytes ...controls_bg.xml => player_controls_bg.xml} | 3 +- .../res/drawable/player_top_controls_bg.xml | 8 + .../main/res/layout/activity_exo_player.xml | 306 ++++- .../res/layout/exomedia_custom_controls.xml | 180 --- .../main/res/layout/player_notification.xml | 4 +- .../layout/player_notification_expanded.xml | 4 +- app/src/main/res/layout/player_popup.xml | 196 ++- .../res/layout/player_popup_notification.xml | 36 +- app/src/main/res/values/strings.xml | 2 + app/src/main/res/values/styles.xml | 2 +- 55 files changed, 2397 insertions(+), 1093 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/player/AbstractPlayer.java rename app/src/main/java/org/schabi/newpipe/player/{popup => }/StateInterface.java (71%) delete mode 100644 app/src/main/java/org/schabi/newpipe/player/popup/PopupViewHolder.java rename app/src/main/res/{drawable-xxhdpi/ic_close_white_24dp.png => drawable-hdpi/ic_close_white.png} (100%) delete mode 100644 app/src/main/res/drawable-hdpi/ic_close_white_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_fullscreen_exit_white.png create mode 100644 app/src/main/res/drawable-hdpi/ic_fullscreen_white.png rename app/src/main/res/{drawable-xxhdpi/ic_pause_white_24dp.png => drawable-hdpi/ic_pause_white.png} (100%) delete mode 100644 app/src/main/res/drawable-hdpi/ic_pause_white_24dp.png rename app/src/main/res/drawable-hdpi/{ic_play_arrow_white_48dp.png => ic_play_arrow_white.png} (100%) create mode 100644 app/src/main/res/drawable-hdpi/ic_repeat_white.png rename app/src/main/res/{drawable-xhdpi/ic_close_white_24dp.png => drawable-mdpi/ic_close_white.png} (100%) delete mode 100644 app/src/main/res/drawable-mdpi/ic_close_white_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_fullscreen_exit_white.png create mode 100644 app/src/main/res/drawable-mdpi/ic_fullscreen_white.png rename app/src/main/res/{drawable-xhdpi/ic_pause_white_24dp.png => drawable-mdpi/ic_pause_white.png} (100%) delete mode 100644 app/src/main/res/drawable-mdpi/ic_pause_white_24dp.png rename app/src/main/res/drawable-mdpi/{ic_play_arrow_white_48dp.png => ic_play_arrow_white.png} (100%) create mode 100644 app/src/main/res/drawable-mdpi/ic_repeat_white.png rename app/src/main/res/{drawable-xxxhdpi/ic_close_white_24dp.png => drawable-xhdpi/ic_close_white.png} (100%) create mode 100644 app/src/main/res/drawable-xhdpi/ic_fullscreen_exit_white.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_fullscreen_white.png rename app/src/main/res/{drawable-xxxhdpi/ic_pause_white_24dp.png => drawable-xhdpi/ic_pause_white.png} (100%) rename app/src/main/res/drawable-xhdpi/{ic_play_arrow_white_48dp.png => ic_play_arrow_white.png} (100%) create mode 100644 app/src/main/res/drawable-xhdpi/ic_repeat_white.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_close_white.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_fullscreen_exit_white.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_fullscreen_white.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_pause_white.png rename app/src/main/res/drawable-xxhdpi/{ic_play_arrow_white_48dp.png => ic_play_arrow_white.png} (100%) create mode 100644 app/src/main/res/drawable-xxhdpi/ic_repeat_white.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_close_white.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_fullscreen_exit_white.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_fullscreen_white.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_pause_white.png rename app/src/main/res/drawable-xxxhdpi/{ic_play_arrow_white_48dp.png => ic_play_arrow_white.png} (100%) create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_repeat_white.png rename app/src/main/res/drawable/{popup_controls_bg.xml => player_controls_bg.xml} (69%) create mode 100644 app/src/main/res/drawable/player_top_controls_bg.xml delete mode 100644 app/src/main/res/layout/exomedia_custom_controls.xml diff --git a/app/build.gradle b/app/build.gradle index 73f11aab0..0f99854c2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -45,7 +45,7 @@ dependencies { compile 'com.google.code.gson:gson:2.4' compile 'com.nononsenseapps:filepicker:3.0.0' compile 'ch.acra:acra:4.9.0' - compile 'com.devbrackets.android:exomedia:3.1.1' + compile 'com.google.android.exoplayer:exoplayer:r2.3.1' testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.10.19' testCompile 'org.json:json:20160810' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a7dcd9205..3172506f9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -51,19 +51,7 @@ android:configChanges="keyboard|keyboardHidden|orientation|screenSize" android:label="@string/app_name" android:launchMode="singleInstance" - android:theme="@style/PlayerTheme"> - - - - - - - - - - - - + android:theme="@style/PlayerTheme"/> + android:label="@string/popup_mode_share_menu_title"> diff --git a/app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailFragment.java b/app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailFragment.java index a221e4e94..f1e94ea8e 100644 --- a/app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailFragment.java @@ -51,6 +51,7 @@ import org.schabi.newpipe.extractor.stream_info.AudioStream; import org.schabi.newpipe.extractor.stream_info.StreamInfo; import org.schabi.newpipe.extractor.stream_info.VideoStream; import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.player.AbstractPlayer; import org.schabi.newpipe.player.BackgroundPlayer; import org.schabi.newpipe.player.ExoPlayerActivity; import org.schabi.newpipe.player.PlayVideoActivity; @@ -59,6 +60,7 @@ import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.util.NavStack; import org.schabi.newpipe.util.PermissionHelper; +import java.util.ArrayList; import java.util.Vector; import static android.app.Activity.RESULT_OK; @@ -331,10 +333,12 @@ public class VideoItemDetailFragment extends Fragment { // so, I can notify the service through a broadcast, but the problem is // when I click in another video, another thumbnail will be load, and will // notify again, so I send the videoUrl and compare with the service's url - ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = streamThumbnail; - Intent intent = new Intent(PopupVideoPlayer.InternalListener.ACTION_UPDATE_THUMB); - intent.putExtra(PopupVideoPlayer.VIDEO_URL, info.webpage_url); - getContext().sendBroadcast(intent); + if (getContext() != null) { + ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = streamThumbnail; + Intent intent = new Intent(AbstractPlayer.ACTION_UPDATE_THUMB); + intent.putExtra(AbstractPlayer.VIDEO_URL, info.webpage_url); + getContext().sendBroadcast(intent); + } } } @@ -388,13 +392,15 @@ public class VideoItemDetailFragment extends Fragment { if (streamThumbnail != null) ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = streamThumbnail; - VideoStream selectedVideoStream = info.video_streams.get(selectedStreamId); Intent i = new Intent(activity, PopupVideoPlayer.class); - Toast.makeText(activity, "Starting in popup mode", Toast.LENGTH_SHORT).show(); - i.putExtra(PopupVideoPlayer.VIDEO_TITLE, info.title) - .putExtra(PopupVideoPlayer.STREAM_URL, selectedVideoStream.url) - .putExtra(PopupVideoPlayer.CHANNEL_NAME, info.uploader) - .putExtra(PopupVideoPlayer.VIDEO_URL, info.webpage_url); + Toast.makeText(activity, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); + i.putExtra(AbstractPlayer.VIDEO_TITLE, info.title) + .putExtra(AbstractPlayer.CHANNEL_NAME, info.uploader) + .putExtra(AbstractPlayer.VIDEO_URL, info.webpage_url) + .putExtra(AbstractPlayer.INDEX_SEL_VIDEO_STREAM, selectedStreamId) + .putExtra(AbstractPlayer.VIDEO_STREAMS_LIST, new ArrayList<>(info.video_streams)); + if (info.start_position > 0) i.putExtra(AbstractPlayer.START_POSITION, info.start_position * 1000); + activity.startService(i); } }); @@ -784,47 +790,27 @@ public class VideoItemDetailFragment extends Fragment { builder.create().show(); } } else { - if (PreferenceManager.getDefaultSharedPreferences(activity) - .getBoolean(activity.getString(R.string.use_exoplayer_key), false)) { - - // TODO: Fix this mess - if (streamThumbnail != null) - ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = streamThumbnail; - // exo player - - if(info.dashMpdUrl != null && !info.dashMpdUrl.isEmpty()) { - // try dash - Intent intent = new Intent(activity, ExoPlayerActivity.class) - .setData(Uri.parse(info.dashMpdUrl)); - //.putExtra(ExoPlayerActivity.CONTENT_TYPE_EXTRA, Util.TYPE_DASH); - startActivity(intent); - } else if((info.audio_streams != null && !info.audio_streams.isEmpty()) && - (info.video_only_streams != null && !info.video_only_streams.isEmpty())) { - // try smooth streaming - - } else { - //default streaming - Intent intent = new Intent(activity, ExoPlayerActivity.class) - .setDataAndType(Uri.parse(selectedVideoStream.url), - MediaFormat.getMimeById(selectedVideoStream.format)) - - .putExtra(ExoPlayerActivity.VIDEO_TITLE, info.title) - .putExtra(ExoPlayerActivity.CHANNEL_NAME, info.uploader); - //.putExtra(ExoPlayerActivity.CONTENT_TYPE_EXTRA, Util.TYPE_OTHER); - - activity.startActivity(intent); // HERE !!! - } - //------------- - + Intent intent; + if (PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(activity.getString(R.string.use_exoplayer_key), false)) { + // ExoPlayer + if (streamThumbnail != null) ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = streamThumbnail; + intent = new Intent(activity, ExoPlayerActivity.class) + .putExtra(AbstractPlayer.VIDEO_TITLE, info.title) + .putExtra(AbstractPlayer.VIDEO_URL, info.webpage_url) + .putExtra(AbstractPlayer.CHANNEL_NAME, info.uploader) + .putExtra(AbstractPlayer.INDEX_SEL_VIDEO_STREAM, actionBarHandler.getSelectedVideoStream()) + .putExtra(AbstractPlayer.VIDEO_STREAMS_LIST, new ArrayList<>(info.video_streams)); + if (info.start_position > 0) intent.putExtra(AbstractPlayer.START_POSITION, info.start_position * 1000); } else { // Internal Player - Intent intent = new Intent(activity, PlayVideoActivity.class) + intent = new Intent(activity, PlayVideoActivity.class) .putExtra(PlayVideoActivity.VIDEO_TITLE, info.title) .putExtra(PlayVideoActivity.STREAM_URL, selectedVideoStream.url) .putExtra(PlayVideoActivity.VIDEO_URL, info.webpage_url) .putExtra(PlayVideoActivity.START_POSITION, info.start_position); - activity.startActivity(intent); //also HERE !!! } + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + activity.startActivity(intent); } // -------------------------------------------- diff --git a/app/src/main/java/org/schabi/newpipe/player/AbstractPlayer.java b/app/src/main/java/org/schabi/newpipe/player/AbstractPlayer.java new file mode 100644 index 000000000..a9987bee9 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/AbstractPlayer.java @@ -0,0 +1,1106 @@ +package org.schabi.newpipe.player; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.net.Uri; +import android.os.Build; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.support.v4.content.ContextCompat; +import android.text.TextUtils; +import android.util.Log; +import android.view.Gravity; +import android.view.Menu; +import android.view.MenuItem; +import android.view.SurfaceView; +import android.view.View; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.PopupMenu; +import android.widget.ProgressBar; +import android.widget.SeekBar; +import android.widget.TextView; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.DefaultLoadControl; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; +import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; +import com.google.android.exoplayer2.source.hls.HlsMediaSource; +import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; +import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; +import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; +import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; +import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; +import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor; +import com.google.android.exoplayer2.upstream.cache.SimpleCache; +import com.google.android.exoplayer2.util.Util; + +import org.schabi.newpipe.ActivityCommunicator; +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.stream_info.VideoStream; + +import java.io.File; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Formatter; +import java.util.List; +import java.util.Locale; +import java.util.Vector; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Common properties of the players + * + * @author mauriciocolli + */ +@SuppressWarnings({"unused", "WeakerAccess"}) +public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBarChangeListener, View.OnClickListener, ExoPlayer.EventListener, PopupMenu.OnMenuItemClickListener, PopupMenu.OnDismissListener, SimpleExoPlayer.VideoListener { + public static final boolean DEBUG = false; + public final String TAG; + + protected Context context; + private SharedPreferences sharedPreferences; + + private static int currentState = -1; + public static final String ACTION_UPDATE_THUMB = "org.schabi.newpipe.player.AbstractPlayer.UPDATE_THUMBNAIL"; + + /*////////////////////////////////////////////////////////////////////////// + // Intent + //////////////////////////////////////////////////////////////////////////*/ + + public static final String VIDEO_URL = "video_url"; + public static final String VIDEO_STREAMS_LIST = "video_streams_list"; + public static final String VIDEO_TITLE = "video_title"; + public static final String INDEX_SEL_VIDEO_STREAM = "index_selected_video_stream"; + public static final String START_POSITION = "start_position"; + public static final String CHANNEL_NAME = "channel_name"; + public static final String STARTED_FROM_NEWPIPE = "started_from_newpipe"; + + private String videoUrl = ""; + private int videoStartPos = -1; + private String videoTitle = ""; + private Bitmap videoThumbnail; + private String channelName = ""; + private int selectedIndexStream; + private ArrayList videoStreamsList; + + /*////////////////////////////////////////////////////////////////////////// + // Player + //////////////////////////////////////////////////////////////////////////*/ + + public static final int FAST_FORWARD_REWIND_AMOUNT = 10000; // 10 Seconds + public static final int DEFAULT_CONTROLS_HIDE_TIME = 3000; // 3 Seconds + public static final String CACHE_FOLDER_NAME = "exoplayer"; + + private boolean startedFromNewPipe = true; + private boolean isPrepared = false; + private boolean wasPlaying = false; + private SimpleExoPlayer simpleExoPlayer; + + @SuppressWarnings("FieldCanBeLocal") + private MediaSource videoSource; + private static CacheDataSourceFactory cacheDataSourceFactory; + private static final DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); + private static final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); + + private AtomicBoolean isProgressLoopRunning = new AtomicBoolean(); + private Handler progressLoop; + private Runnable progressUpdate; + + /*////////////////////////////////////////////////////////////////////////// + // Repeat + //////////////////////////////////////////////////////////////////////////*/ + + private RepeatMode currentRepeatMode = RepeatMode.REPEAT_DISABLED; + + public enum RepeatMode { + REPEAT_DISABLED, + REPEAT_ONE, + REPEAT_ALL + } + + /*////////////////////////////////////////////////////////////////////////// + // Views + //////////////////////////////////////////////////////////////////////////*/ + + private View rootView; + + private AspectRatioFrameLayout aspectRatioFrameLayout; + private SurfaceView surfaceView; + private View surfaceForeground; + + private View loadingPanel; + private ImageView endScreen; + private ImageView controlAnimationView; + + private View controlsRoot; + private TextView currentDisplaySeek; + + private View bottomControlsRoot; + private SeekBar playbackSeekBar; + private TextView playbackCurrentTime; + private TextView playbackEndTime; + + private View topControlsRoot; + private TextView qualityTextView; + private ImageButton fullScreenButton; + + private ValueAnimator controlViewAnimator; + + private boolean isQualityPopupMenuVisible = false; + private boolean qualityChanged = false; + private int qualityPopupMenuGroupId = 69; + private PopupMenu qualityPopupMenu; + + /////////////////////////////////////////////////////////////////////////// + + public AbstractPlayer(String debugTag, Context context) { + this.TAG = debugTag; + this.context = context; + this.progressLoop = new Handler(); + this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + + if (cacheDataSourceFactory == null) { + DefaultDataSourceFactory dataSourceFactory = new DefaultDataSourceFactory(context, Util.getUserAgent(context, context.getPackageName()), bandwidthMeter); + File cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME); + if (!cacheDir.exists()) { + //noinspection ResultOfMethodCallIgnored + cacheDir.mkdir(); + } + + Log.d(TAG, "buildMediaSource: cacheDir = " + cacheDir.getAbsolutePath()); + SimpleCache simpleCache = new SimpleCache(cacheDir, new LeastRecentlyUsedCacheEvictor(64 * 1024 * 1024L)); + cacheDataSourceFactory = new CacheDataSourceFactory(simpleCache, dataSourceFactory, CacheDataSource.FLAG_BLOCK_ON_CACHE, 512 * 1024); + } + } + + public void setup(View rootView) { + initViews(rootView); + initListeners(); + if (simpleExoPlayer == null) initPlayer(); + else { + simpleExoPlayer.addListener(this); + simpleExoPlayer.setVideoListener(this); + simpleExoPlayer.setVideoSurfaceView(surfaceView); + } + } + + public void initViews(View rootView) { + this.rootView = rootView; + this.aspectRatioFrameLayout = (AspectRatioFrameLayout) rootView.findViewById(R.id.aspectRatioLayout); + this.surfaceView = (SurfaceView) rootView.findViewById(R.id.surfaceView); + this.surfaceForeground = rootView.findViewById(R.id.surfaceForeground); + this.loadingPanel = rootView.findViewById(R.id.loadingPanel); + this.endScreen = (ImageView) rootView.findViewById(R.id.endScreen); + this.controlAnimationView = (ImageView) rootView.findViewById(R.id.controlAnimationView); + this.controlsRoot = rootView.findViewById(R.id.playbackControlRoot); + this.currentDisplaySeek = (TextView) rootView.findViewById(R.id.currentDisplaySeek); + this.playbackSeekBar = (SeekBar) rootView.findViewById(R.id.playbackSeekBar); + this.playbackCurrentTime = (TextView) rootView.findViewById(R.id.playbackCurrentTime); + this.playbackEndTime = (TextView) rootView.findViewById(R.id.playbackEndTime); + this.bottomControlsRoot = rootView.findViewById(R.id.bottomControls); + this.topControlsRoot = rootView.findViewById(R.id.topControls); + this.qualityTextView = (TextView) rootView.findViewById(R.id.qualityTextView); + this.fullScreenButton = (ImageButton) rootView.findViewById(R.id.fullScreenButton); + + //this.aspectRatioFrameLayout.setAspectRatio(16.0f / 9.0f); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); + this.playbackSeekBar.getProgressDrawable().setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) this.qualityPopupMenu = new PopupMenu(context, qualityTextView, Gravity.CENTER | Gravity.BOTTOM); + else this.qualityPopupMenu = new PopupMenu(context, qualityTextView); + + ((ProgressBar) this.loadingPanel.findViewById(R.id.progressBarLoadingPanel)).getIndeterminateDrawable().setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY); + + } + + public void initListeners() { + progressUpdate = new Runnable() { + @Override + public void run() { + //if(DEBUG) Log.d(TAG, "progressUpdate run() called"); + onUpdateProgress((int) simpleExoPlayer.getCurrentPosition(), (int) simpleExoPlayer.getDuration(), simpleExoPlayer.getBufferedPercentage()); + if (isProgressLoopRunning.get()) progressLoop.postDelayed(this, 100); + } + }; + + playbackSeekBar.setOnSeekBarChangeListener(this); + fullScreenButton.setOnClickListener(this); + qualityTextView.setOnClickListener(this); + } + + public void initPlayer() { + if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]"); + + AdaptiveTrackSelection.Factory trackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter); + DefaultTrackSelector defaultTrackSelector = new DefaultTrackSelector(trackSelectionFactory); + DefaultLoadControl loadControl = new DefaultLoadControl(); + + simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(context, defaultTrackSelector, loadControl); + simpleExoPlayer.addListener(this); + simpleExoPlayer.setVideoListener(this); + simpleExoPlayer.setVideoSurfaceView(surfaceView); + } + + @SuppressWarnings("unchecked") + public void handleIntent(Intent intent) { + if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]"); + if (intent == null) return; + + selectedIndexStream = intent.getIntExtra(INDEX_SEL_VIDEO_STREAM, -1); + + Serializable serializable = intent.getSerializableExtra(VIDEO_STREAMS_LIST); + + if (serializable instanceof ArrayList) videoStreamsList = (ArrayList) serializable; + if (serializable instanceof Vector) videoStreamsList = new ArrayList<>((List) serializable); + + videoUrl = intent.getStringExtra(VIDEO_URL); + videoTitle = intent.getStringExtra(VIDEO_TITLE); + videoStartPos = intent.getIntExtra(START_POSITION, -1); + channelName = intent.getStringExtra(CHANNEL_NAME); + startedFromNewPipe = intent.getBooleanExtra(STARTED_FROM_NEWPIPE, true); + try { + videoThumbnail = ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail; + } catch (Exception e) { + e.printStackTrace(); + } + + playVideo(getSelectedStreamUri(), true); + } + + public void playVideo(Uri videoURI, boolean autoPlay) { + if (DEBUG) Log.d(TAG, "playVideo() called with: videoURI = [" + videoURI + "], autoPlay = [" + autoPlay + "]"); + + if (videoURI == null || simpleExoPlayer == null) { + onError(); + return; + } + + changeState(STATE_LOADING); + isPrepared = false; + qualityChanged = false; + + qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId); + buildQualityMenu(qualityPopupMenu); + + videoSource = buildMediaSource(videoURI, MediaFormat.getSuffixById(videoStreamsList.get(selectedIndexStream).format)); + + if (simpleExoPlayer.getPlaybackState() != ExoPlayer.STATE_IDLE) simpleExoPlayer.stop(); + if (videoStartPos > 0) simpleExoPlayer.seekTo(videoStartPos); + simpleExoPlayer.prepare(videoSource); + simpleExoPlayer.setPlayWhenReady(autoPlay); + } + + public void destroy() { + if (DEBUG) Log.d(TAG, "destroy() called"); + if (simpleExoPlayer != null) { + simpleExoPlayer.stop(); + simpleExoPlayer.release(); + } + if (progressLoop != null) stopProgressLoop(); + } + + private MediaSource buildMediaSource(Uri uri, String overrideExtension) { + if (DEBUG) Log.d(TAG, "buildMediaSource() called with: uri = [" + uri + "], overrideExtension = [" + overrideExtension + "]"); + int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); + switch (type) { + case C.TYPE_SS: + return new SsMediaSource(uri, cacheDataSourceFactory, new DefaultSsChunkSource.Factory(cacheDataSourceFactory), null, null); + case C.TYPE_DASH: + return new DashMediaSource(uri, cacheDataSourceFactory, new DefaultDashChunkSource.Factory(cacheDataSourceFactory), null, null); + case C.TYPE_HLS: + return new HlsMediaSource(uri, cacheDataSourceFactory, null, null); + case C.TYPE_OTHER: + return new ExtractorMediaSource(uri, cacheDataSourceFactory, extractorsFactory, null, null); + default: { + throw new IllegalStateException("Unsupported type: " + type); + } + } + } + + public void buildQualityMenu(PopupMenu popupMenu) { + for (int i = 0; i < videoStreamsList.size(); i++) { + VideoStream videoStream = videoStreamsList.get(i); + popupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE, MediaFormat.getNameById(videoStream.format) + " " + videoStream.resolution); + } + qualityTextView.setText(videoStreamsList.get(selectedIndexStream).resolution); + popupMenu.setOnMenuItemClickListener(this); + popupMenu.setOnDismissListener(this); + + } + + /*////////////////////////////////////////////////////////////////////////// + // States Implementation + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void changeState(int state) { + if (DEBUG) Log.d(TAG, "changeState() called with: state = [" + state + "]"); + currentState = state; + switch (state) { + case STATE_LOADING: + onLoading(); + break; + case STATE_PLAYING: + onPlaying(); + break; + case STATE_BUFFERING: + onBuffering(); + break; + case STATE_PAUSED: + onPaused(); + break; + case STATE_PAUSED_SEEK: + onPausedSeek(); + break; + case STATE_COMPLETED: + onCompleted(); + break; + } + } + + @Override + public void onLoading() { + if (DEBUG) Log.d(TAG, "onLoading() called"); + + if (!isProgressLoopRunning.get()) startProgressLoop(); + + showAndAnimateControl(-1, true); + playbackSeekBar.setEnabled(true); + playbackSeekBar.setProgress(0); + + // Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, so sets the color again + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); + + animateView(endScreen, false, 0, 0); + animateView(controlsRoot, false, 0, 0); + loadingPanel.setBackgroundColor(Color.BLACK); + animateView(loadingPanel, true, 0, 0); + animateView(surfaceForeground, true, 100, 0); + } + + @Override + public void onPlaying() { + if (DEBUG) Log.d(TAG, "onPlaying() called"); + if (!isProgressLoopRunning.get()) startProgressLoop(); + showAndAnimateControl(-1, true); + loadingPanel.setVisibility(View.GONE); + animateView(controlsRoot, false, 500, DEFAULT_CONTROLS_HIDE_TIME, true); + animateView(currentDisplaySeek, false, 200, 0); + } + + @Override + public void onBuffering() { + if (DEBUG) Log.d(TAG, "onBuffering() called"); + loadingPanel.setBackgroundColor(Color.TRANSPARENT); + animateView(loadingPanel, true, 500, 0); + animateView(controlsRoot, false, 0, 0); + } + + @Override + public void onPaused() { + if (DEBUG) Log.d(TAG, "onPaused() called"); + animateView(controlsRoot, true, 500, 100); + loadingPanel.setVisibility(View.GONE); + } + + @Override + public void onPausedSeek() { + if (DEBUG) Log.d(TAG, "onPausedSeek() called"); + showAndAnimateControl(-1, true); + } + + @Override + public void onCompleted() { + if (DEBUG) Log.d(TAG, "onCompleted() called"); + + if (isProgressLoopRunning.get()) stopProgressLoop(); + + if (videoThumbnail != null) endScreen.setImageBitmap(videoThumbnail); + animateView(controlsRoot, true, 500, 0); + animateView(endScreen, true, 800, 0); + animateView(currentDisplaySeek, false, 200, 0); + loadingPanel.setVisibility(View.GONE); + + playbackSeekBar.setMax((int) simpleExoPlayer.getDuration()); + playbackSeekBar.setProgress(playbackSeekBar.getMax()); + playbackSeekBar.setEnabled(false); + playbackEndTime.setText(getTimeString(playbackSeekBar.getMax())); + playbackCurrentTime.setText(playbackEndTime.getText()); + // Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, so sets the color again + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); + + animateView(surfaceForeground, true, 100, 0); + + if (currentRepeatMode == RepeatMode.REPEAT_ONE) { + changeState(STATE_LOADING); + getPlayer().seekTo(0); + } + } + + /*////////////////////////////////////////////////////////////////////////// + // ExoPlayer Listener + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onTimelineChanged(Timeline timeline, Object manifest) { + + } + + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + + } + + @Override + public void onLoadingChanged(boolean isLoading) { + if (DEBUG) Log.d(TAG, "onLoadingChanged() called with: isLoading = [" + isLoading + "]"); + + if (!isLoading && getCurrentState() == STATE_PAUSED && isProgressLoopRunning.get()) stopProgressLoop(); + else if (isLoading && !isProgressLoopRunning.get()) startProgressLoop(); + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (DEBUG) Log.d(TAG, "onPlayerStateChanged() called with: playWhenReady = [" + playWhenReady + "], playbackState = [" + playbackState + "]"); + if (getCurrentState() == STATE_PAUSED_SEEK) { + if (DEBUG) Log.d(TAG, "onPlayerStateChanged() currently on PausedSeek"); + return; + } + + switch (playbackState) { + case ExoPlayer.STATE_IDLE: // 1 + isPrepared = false; + break; + case ExoPlayer.STATE_BUFFERING: // 2 + if (isPrepared && getCurrentState() != STATE_LOADING) changeState(STATE_BUFFERING); + break; + case ExoPlayer.STATE_READY: //3 + if (!isPrepared) { + isPrepared = true; + onPrepared(playWhenReady); + break; + } + if (currentState == STATE_PAUSED_SEEK) break; + changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); + break; + case ExoPlayer.STATE_ENDED: // 4 + changeState(STATE_COMPLETED); + isPrepared = false; + break; + } + } + + @Override + public void onPlayerError(ExoPlaybackException error) { + if (DEBUG) Log.d(TAG, "onPlayerError() called with: error = [" + error + "]"); + onError(); + } + + @Override + public void onPositionDiscontinuity() { + if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called"); + } + + /*////////////////////////////////////////////////////////////////////////// + // ExoPlayer Video Listener + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { + if (DEBUG) { + Log.d(TAG, "onVideoSizeChanged() called with: width / height = [" + width + " / " + height + " = " + (((float) width) / height) + "], unappliedRotationDegrees = [" + unappliedRotationDegrees + "], pixelWidthHeightRatio = [" + pixelWidthHeightRatio + "]"); + } + aspectRatioFrameLayout.setAspectRatio(((float) width) / height); + } + + @Override + public void onRenderedFirstFrame() { + animateView(surfaceForeground, false, 100, 0); + } + + /*////////////////////////////////////////////////////////////////////////// + // General Player + //////////////////////////////////////////////////////////////////////////*/ + + public abstract void onError(); + + public void onPrepared(boolean playWhenReady) { + if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); + + if (videoStartPos > 0) { + playbackSeekBar.setProgress(videoStartPos); + playbackCurrentTime.setText(getTimeString(videoStartPos)); + videoStartPos = -1; + } + + playbackSeekBar.setMax((int) simpleExoPlayer.getDuration()); + playbackEndTime.setText(getTimeString((int) simpleExoPlayer.getDuration())); + + changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); + } + + public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) { + if (!isPrepared) return; + if (currentState != STATE_PAUSED) { + if (currentState != STATE_PAUSED_SEEK) playbackSeekBar.setProgress(currentProgress); + playbackCurrentTime.setText(getTimeString(currentProgress)); + } + if (simpleExoPlayer.isLoading() || bufferPercent > 90) { + playbackSeekBar.setSecondaryProgress((int) (playbackSeekBar.getMax() * ((float) bufferPercent / 100))); + } + if (DEBUG && bufferPercent % 20 == 0) { //Limit log + Log.d(TAG, "updateProgress() called with: isVisible = " + isControlsVisible() + ", currentProgress = [" + currentProgress + "], duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]"); + } + } + + public void onUpdateThumbnail(Intent intent) { + if (DEBUG) Log.d(TAG, "onUpdateThumbnail() called with: intent = [" + intent + "]"); + if (!intent.getStringExtra(VIDEO_URL).equals(videoUrl)) return; + videoThumbnail = ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail; + } + + public void onVideoPlayPause() { + if (DEBUG) Log.d(TAG, "onVideoPlayPause() called"); + if (currentState == STATE_COMPLETED) { + changeState(STATE_LOADING); + if (qualityChanged) playVideo(getSelectedStreamUri(), true); + simpleExoPlayer.seekTo(0); + return; + } + simpleExoPlayer.setPlayWhenReady(!isPlaying()); + } + + public void onFastRewind() { + if (DEBUG) Log.d(TAG, "onFastRewind() called"); + seekBy(-FAST_FORWARD_REWIND_AMOUNT); + showAndAnimateControl(R.drawable.ic_action_av_fast_rewind, true); + animateView(controlsRoot, false, 100, 0); + } + + public void onFastForward() { + if (DEBUG) Log.d(TAG, "onFastForward() called"); + seekBy(FAST_FORWARD_REWIND_AMOUNT); + showAndAnimateControl(R.drawable.ic_action_av_fast_forward, true); + animateView(controlsRoot, false, 100, 0); + } + + /*////////////////////////////////////////////////////////////////////////// + // OnClick related + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onClick(View v) { + if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]"); + if (v.getId() == fullScreenButton.getId()) { + onFullScreenButtonClicked(); + } else if (v.getId() == qualityTextView.getId()) { + onQualitySelectorClicked(); + } + } + + /** + * Called when an item of the quality selector is selected + */ + @Override + public boolean onMenuItemClick(MenuItem menuItem) { + if (DEBUG) Log.d(TAG, "onMenuItemClick() called with: menuItem = [" + menuItem + "], menuItem.getItemId = [" + menuItem.getItemId() + "]"); + if (selectedIndexStream == menuItem.getItemId()) return true; + setVideoStartPos((int) getPlayer().getCurrentPosition()); + + if (!(getCurrentState() == STATE_COMPLETED)) playVideo(Uri.parse(getVideoStreamsList().get(menuItem.getItemId()).url), wasPlaying); + else qualityChanged = true; + + selectedIndexStream = menuItem.getItemId(); + qualityTextView.setText(menuItem.getTitle()); + return true; + } + + /** + * Called when the quality selector is dismissed + */ + @Override + public void onDismiss(PopupMenu menu) { + if (DEBUG) Log.d(TAG, "onDismiss() called with: menu = [" + menu + "]"); + isQualityPopupMenuVisible = false; + qualityTextView.setText(videoStreamsList.get(selectedIndexStream).resolution); + } + + public abstract void onFullScreenButtonClicked(); + + public void onQualitySelectorClicked() { + if (DEBUG) Log.d(TAG, "onQualitySelectorClicked() called"); + qualityPopupMenu.show(); + isQualityPopupMenuVisible = true; + animateView(getControlsRoot(), true, 300, 0); + + VideoStream videoStream = videoStreamsList.get(selectedIndexStream); + qualityTextView.setText(MediaFormat.getNameById(videoStream.format) + " " + videoStream.resolution); + wasPlaying = isPlaying(); + } + + public void onRepeatClicked() { + if (DEBUG) Log.d(TAG, "onRepeatClicked() called"); + // TODO: implement repeat all when playlist is implemented + + // Switch the modes between DISABLED and REPEAT_ONE, till playlist is implemented + setCurrentRepeatMode(getCurrentRepeatMode() == RepeatMode.REPEAT_DISABLED ? + RepeatMode.REPEAT_ONE : + RepeatMode.REPEAT_DISABLED); + + if (DEBUG) Log.d(TAG, "onRepeatClicked() currentRepeatMode = " + getCurrentRepeatMode().name()); + } + + /*////////////////////////////////////////////////////////////////////////// + // SeekBar Listener + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (DEBUG && fromUser) Log.d(TAG, "onProgressChanged() called with: seekBar = [" + seekBar + "], progress = [" + progress + "]"); + //if (fromUser) playbackCurrentTime.setText(getTimeString(progress)); + if (fromUser) currentDisplaySeek.setText(getTimeString(progress)); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + if (DEBUG) Log.d(TAG, "onStartTrackingTouch() called with: seekBar = [" + seekBar + "]"); + if (getCurrentState() != STATE_PAUSED_SEEK) changeState(STATE_PAUSED_SEEK); + + wasPlaying = isPlaying(); + if (isPlaying()) simpleExoPlayer.setPlayWhenReady(false); + + animateView(controlsRoot, true, 0, 0); + animateView(currentDisplaySeek, true, 300, 0); + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + if (DEBUG) Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]"); + + simpleExoPlayer.seekTo(seekBar.getProgress()); + if (wasPlaying || simpleExoPlayer.getDuration() == seekBar.getProgress()) simpleExoPlayer.setPlayWhenReady(true); + + playbackCurrentTime.setText(getTimeString(seekBar.getProgress())); + animateView(currentDisplaySeek, false, 200, 0); + + if (getCurrentState() == STATE_PAUSED_SEEK) changeState(STATE_BUFFERING); + if (!isProgressLoopRunning.get()) startProgressLoop(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + private static final StringBuilder stringBuilder = new StringBuilder(); + private static final Formatter formatter = new Formatter(stringBuilder, Locale.getDefault()); + + public String getTimeString(int milliSeconds) { + long seconds = (milliSeconds % 60000L) / 1000L; + long minutes = (milliSeconds % 3600000L) / 60000L; + long hours = (milliSeconds % 86400000L) / 3600000L; + long days = (milliSeconds % (86400000L * 7L)) / 86400000L; + + stringBuilder.setLength(0); + return days > 0 ? formatter.format("%d:%02d:%02d:%02d", days, hours, minutes, seconds).toString() + : hours > 0 ? formatter.format("%d:%02d:%02d", hours, minutes, seconds).toString() + : formatter.format("%02d:%02d", minutes, seconds).toString(); + } + + public boolean isControlsVisible() { + return controlsRoot != null && controlsRoot.getVisibility() == View.VISIBLE; + } + + /** + * Show a animation, and depending on goneOnEnd, will stay on the screen or be gone + * + * @param drawableId the drawable that will be used to animate, pass -1 to clear any animation that is visible + * @param goneOnEnd will set the animation view to GONE on the end of the animation + */ + public void showAndAnimateControl(final int drawableId, final boolean goneOnEnd) { + if (DEBUG) Log.d(TAG, "showAndAnimateControl() called with: drawableId = [" + drawableId + "], goneOnEnd = [" + goneOnEnd + "]"); + if (controlViewAnimator != null && controlViewAnimator.isRunning()) { + if (DEBUG) Log.d(TAG, "showAndAnimateControl: controlViewAnimator.isRunning"); + controlViewAnimator.end(); + } + + if (drawableId == -1) { + if (controlAnimationView.getVisibility() == View.VISIBLE) { + controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(controlAnimationView, + PropertyValuesHolder.ofFloat(View.ALPHA, 1f, 0f), + PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1f), + PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1f) + ).setDuration(300); + controlViewAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + controlAnimationView.setVisibility(View.GONE); + } + }); + controlViewAnimator.start(); + } + return; + } + + float scaleFrom = goneOnEnd ? 1f : 1f, scaleTo = goneOnEnd ? 1.8f : 1.4f; + float alphaFrom = goneOnEnd ? 1f : 0f, alphaTo = goneOnEnd ? 0f : 1f; + + + controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(controlAnimationView, + PropertyValuesHolder.ofFloat(View.ALPHA, alphaFrom, alphaTo), + PropertyValuesHolder.ofFloat(View.SCALE_X, scaleFrom, scaleTo), + PropertyValuesHolder.ofFloat(View.SCALE_Y, scaleFrom, scaleTo) + ); + controlViewAnimator.setDuration(goneOnEnd ? 1000 : 500); + controlViewAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (goneOnEnd) controlAnimationView.setVisibility(View.GONE); + else controlAnimationView.setVisibility(View.VISIBLE); + } + }); + + + controlAnimationView.setVisibility(View.VISIBLE); + controlAnimationView.setImageDrawable(ContextCompat.getDrawable(context, drawableId)); + controlViewAnimator.start(); + } + + public void animateView(View view, boolean enterOrExit, long duration, long delay) { + animateView(view, enterOrExit, duration, delay, null, false); + } + + public void animateView(View view, boolean enterOrExit, long duration, long delay, boolean hideUi) { + animateView(view, enterOrExit, duration, delay, null, hideUi); + } + + public void animateView(final View view, final boolean enterOrExit, long duration, long delay, final Runnable execOnEnd) { + animateView(view, enterOrExit, duration, delay, execOnEnd, false); + } + + /** + * Animate the view + * + * @param view view that will be animated + * @param enterOrExit true to enter, false to exit + * @param duration how long the animation will take, in milliseconds + * @param delay how long the animation will wait to start, in milliseconds + * @param execOnEnd runnable that will be executed when the animation ends + * @param hideUi need to hide ui when animation ends, + * just a helper for classes extending this + */ + public void animateView(final View view, final boolean enterOrExit, long duration, long delay, final Runnable execOnEnd, boolean hideUi) { + if (DEBUG) { + Log.d(TAG, "animateView() called with: view = [" + view + "], enterOrExit = [" + enterOrExit + "], duration = [" + duration + "], delay = [" + delay + "], execOnEnd = [" + execOnEnd + "]"); + } + if (view.getVisibility() == View.VISIBLE && enterOrExit) { + if (DEBUG) Log.d(TAG, "animateView() view was already visible > view = [" + view + "]"); + view.animate().setListener(null).cancel(); + view.setVisibility(View.VISIBLE); + view.setAlpha(1f); + if (execOnEnd != null) execOnEnd.run(); + return; + } else if ((view.getVisibility() == View.GONE || view.getVisibility() == View.INVISIBLE) && !enterOrExit) { + if (DEBUG) Log.d(TAG, "animateView() view was already gone > view = [" + view + "]"); + view.animate().setListener(null).cancel(); + view.setVisibility(View.GONE); + view.setAlpha(0f); + if (execOnEnd != null) execOnEnd.run(); + return; + } + + view.animate().setListener(null).cancel(); + view.setVisibility(View.VISIBLE); + + if (view == controlsRoot) { + if (enterOrExit) { + view.animate().alpha(1f).setDuration(duration).setStartDelay(delay) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (execOnEnd != null) execOnEnd.run(); + } + }).start(); + } else { + view.animate().alpha(0f) + .setDuration(duration).setStartDelay(delay) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setVisibility(View.GONE); + if (execOnEnd != null) execOnEnd.run(); + } + }) + .start(); + } + return; + } + + if (enterOrExit) { + view.setAlpha(0f); + view.setScaleX(.8f); + view.setScaleY(.8f); + view.animate().alpha(1f).scaleX(1f).scaleY(1f).setDuration(duration).setStartDelay(delay) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (execOnEnd != null) execOnEnd.run(); + } + }).start(); + } else { + view.setAlpha(1f); + view.setScaleX(1f); + view.setScaleY(1f); + view.animate().alpha(0f).scaleX(.8f).scaleY(.8f).setDuration(duration).setStartDelay(delay) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setVisibility(View.GONE); + if (execOnEnd != null) execOnEnd.run(); + } + }) + .start(); + } + } + + private void seekBy(int milliSeconds) { + if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]"); + if (simpleExoPlayer == null) return; + int progress = (int) (simpleExoPlayer.getCurrentPosition() + milliSeconds); + simpleExoPlayer.seekTo(progress); + } + + public boolean isPlaying() { + return simpleExoPlayer.getPlaybackState() == ExoPlayer.STATE_READY && simpleExoPlayer.getPlayWhenReady(); + } + + public boolean isQualityMenuVisible() { + return isQualityPopupMenuVisible; + } + + private void startProgressLoop() { + progressLoop.removeCallbacksAndMessages(null); + isProgressLoopRunning.set(true); + progressLoop.post(progressUpdate); + } + + private void stopProgressLoop() { + isProgressLoopRunning.set(false); + progressLoop.removeCallbacksAndMessages(null); + } + + public void tryDeleteCacheFiles(Context context) { + File cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME); + + if (cacheDir.exists()) { + try { + if (cacheDir.isDirectory()) { + for (File file : cacheDir.listFiles()) { + try { + if (DEBUG) Log.d(TAG, "tryDeleteCacheFiles: " + file.getAbsolutePath() + " deleted = " + file.delete()); + } catch (Exception ignored) { + } + } + } + } catch (Exception ignored) { + } + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Getters and Setters + //////////////////////////////////////////////////////////////////////////*/ + + public SimpleExoPlayer getPlayer() { + return simpleExoPlayer; + } + + public SharedPreferences getSharedPreferences() { + return sharedPreferences; + } + + public AspectRatioFrameLayout getAspectRatioFrameLayout() { + return aspectRatioFrameLayout; + } + + public SurfaceView getSurfaceView() { + return surfaceView; + } + + public RepeatMode getCurrentRepeatMode() { + return currentRepeatMode; + } + + public void setCurrentRepeatMode(RepeatMode mode) { + currentRepeatMode = mode; + } + + public boolean wasPlaying() { + return wasPlaying; + } + + public int getCurrentState() { + return currentState; + } + + public Uri getSelectedStreamUri() { + return Uri.parse(videoStreamsList.get(selectedIndexStream).url); + } + + public int getQualityPopupMenuGroupId() { + return qualityPopupMenuGroupId; + } + + public String getVideoUrl() { + return videoUrl; + } + + public void setVideoUrl(String videoUrl) { + this.videoUrl = videoUrl; + } + + public int getVideoStartPos() { + return videoStartPos; + } + + public void setVideoStartPos(int videoStartPos) { + this.videoStartPos = videoStartPos; + } + + public String getVideoTitle() { + return videoTitle; + } + + public void setVideoTitle(String videoTitle) { + this.videoTitle = videoTitle; + } + + public Bitmap getVideoThumbnail() { + return videoThumbnail; + } + + public void setVideoThumbnail(Bitmap videoThumbnail) { + this.videoThumbnail = videoThumbnail; + } + + public String getChannelName() { + return channelName; + } + + public void setChannelName(String channelName) { + this.channelName = channelName; + } + + public int getSelectedIndexStream() { + return selectedIndexStream; + } + + public void setSelectedIndexStream(int selectedIndexStream) { + this.selectedIndexStream = selectedIndexStream; + } + + public ArrayList getVideoStreamsList() { + return videoStreamsList; + } + + public void setVideoStreamsList(ArrayList videoStreamsList) { + this.videoStreamsList = videoStreamsList; + } + + public boolean isStartedFromNewPipe() { + return startedFromNewPipe; + } + + public void setStartedFromNewPipe(boolean startedFromNewPipe) { + this.startedFromNewPipe = startedFromNewPipe; + } + + public View getRootView() { + return rootView; + } + + public void setRootView(View rootView) { + this.rootView = rootView; + } + + public View getLoadingPanel() { + return loadingPanel; + } + + public ImageView getEndScreen() { + return endScreen; + } + + public ImageView getControlAnimationView() { + return controlAnimationView; + } + + public View getControlsRoot() { + return controlsRoot; + } + + public View getBottomControlsRoot() { + return bottomControlsRoot; + } + + public SeekBar getPlaybackSeekBar() { + return playbackSeekBar; + } + + public TextView getPlaybackCurrentTime() { + return playbackCurrentTime; + } + + public TextView getPlaybackEndTime() { + return playbackEndTime; + } + + public View getTopControlsRoot() { + return topControlsRoot; + } + + public TextView getQualityTextView() { + return qualityTextView; + } + + public ImageButton getFullScreenButton() { + return fullScreenButton; + } + + public PopupMenu getQualityPopupMenu() { + return qualityPopupMenu; + } + + public View getSurfaceForeground() { + return surfaceForeground; + } + + public TextView getCurrentDisplaySeek() { + return currentDisplaySeek; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index d1c53d85a..88a5a914d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -26,7 +26,6 @@ import org.schabi.newpipe.ActivityCommunicator; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; import org.schabi.newpipe.detail.VideoItemDetailActivity; -import org.schabi.newpipe.detail.VideoItemDetailFragment; import org.schabi.newpipe.util.NavStack; import java.io.IOException; @@ -343,7 +342,7 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare /* NotificationCompat.Action pauseButton = new NotificationCompat.Action.Builder - (R.drawable.ic_pause_white_24dp, "Pause", playPI).build(); + (R.drawable.ic_pause_white, "Pause", playPI).build(); */ PendingIntent playPI = PendingIntent.getBroadcast(owner, noteID, @@ -465,7 +464,7 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare RemoteViews views = getContentView(), bigViews = getBigContentView(); int imageSrc; if(isPlaying) { - imageSrc = R.drawable.ic_pause_white_24dp; + imageSrc = R.drawable.ic_pause_white; } else { imageSrc = R.drawable.ic_play_circle_filled_white_24dp; } diff --git a/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java index c868bb722..58e93fc61 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java @@ -1,220 +1,559 @@ package org.schabi.newpipe.player; import android.app.Activity; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Color; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.support.annotation.Nullable; import android.util.Log; +import android.view.GestureDetector; +import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; import android.widget.ImageButton; +import android.widget.PopupMenu; import android.widget.SeekBar; - -import com.devbrackets.android.exomedia.listener.OnCompletionListener; -import com.devbrackets.android.exomedia.listener.OnPreparedListener; -import com.devbrackets.android.exomedia.listener.VideoControlsVisibilityListener; -import com.devbrackets.android.exomedia.ui.widget.EMVideoView; -import com.devbrackets.android.exomedia.ui.widget.VideoControlsMobile; +import android.widget.TextView; +import android.widget.Toast; import org.schabi.newpipe.R; +import org.schabi.newpipe.util.NavStack; +import org.schabi.newpipe.util.PermissionHelper; +import org.schabi.newpipe.util.ThemeHelper; -public class ExoPlayerActivity extends Activity implements OnPreparedListener, OnCompletionListener { - private static final String TAG = "ExoPlayerActivity"; - private static final boolean DEBUG = false; - private EMVideoView videoView; - private CustomVideoControls videoControls; +/** + * Activity Player implementing AbstractPlayer + * + * @author mauriciocolli + */ +public class ExoPlayerActivity extends Activity { + private static final String TAG = ".ExoPlayerActivity"; + private static final boolean DEBUG = AbstractPlayer.DEBUG; - public static final String VIDEO_TITLE = "video_title"; - public static final String CHANNEL_NAME = "channel_name"; - private String videoTitle = ""; - private volatile String channelName = ""; - private int lastPosition; - private boolean isFinished; + private AudioManager audioManager; + private BroadcastReceiver broadcastReceiver; + private GestureDetector gestureDetector; + + private final Runnable hideUiRunnable = new Runnable() { + @Override + public void run() { + hideSystemUi(); + } + }; + private boolean activityPaused; + + private AbstractPlayerImpl playerImpl; + + /*////////////////////////////////////////////////////////////////////////// + // Activity LifeCycle + //////////////////////////////////////////////////////////////////////////*/ @Override - public void onCreate(Bundle savedInstanceState) { + protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); + if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]"); + ThemeHelper.setTheme(this, false); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) getWindow().setStatusBarColor(Color.BLACK); + setVolumeControlStream(AudioManager.STREAM_MUSIC); + audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + + if (getIntent() == null) { + Toast.makeText(this, R.string.general_error, Toast.LENGTH_SHORT).show(); + finish(); + return; + } + setContentView(R.layout.activity_exo_player); - videoView = (EMVideoView) findViewById(R.id.emVideoView); + playerImpl = new AbstractPlayerImpl(); + playerImpl.setup(findViewById(android.R.id.content)); + initReceiver(); + playerImpl.handleIntent(getIntent()); } @Override - protected void onStart() { - super.onStart(); - Intent intent = getIntent(); - videoTitle = intent.getStringExtra(VIDEO_TITLE); - channelName = intent.getStringExtra(CHANNEL_NAME); - videoView.setOnPreparedListener(this); - videoView.setOnCompletionListener(this); - videoView.setVideoURI(intent.getData()); - - videoControls = new CustomVideoControls(this); - videoControls.setTitle(videoTitle); - videoControls.setSubTitle(channelName); - - //We don't need these button until the playlist or queue is implemented - videoControls.setNextButtonRemoved(true); - videoControls.setPreviousButtonRemoved(true); - - videoControls.setVisibilityListener(new VideoControlsVisibilityListener() { - @Override - public void onControlsShown() { - if (DEBUG) Log.d(TAG, "------------ onControlsShown() called"); - showSystemUi(); - } - - @Override - public void onControlsHidden() { - if (DEBUG) Log.d(TAG, "------------ onControlsHidden() called"); - hideSystemUi(); - } - }); - videoView.setControls(videoControls); + protected void onNewIntent(Intent intent) { + if (DEBUG) Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]"); + super.onNewIntent(intent); + playerImpl.handleIntent(intent); } @Override - public void onPrepared() { - if (DEBUG) Log.d(TAG, "onPrepared() called"); - videoView.start(); + public void onBackPressed() { + if (DEBUG) Log.d(TAG, "onBackPressed() called"); + super.onBackPressed(); + if (playerImpl.isStartedFromNewPipe()) NavStack.getInstance().openDetailActivity(this, playerImpl.getVideoUrl(), 0); + if (playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(false); } @Override - public void onCompletion() { - if (DEBUG) Log.d(TAG, "onCompletion() called"); -// videoView.getVideoControls().setButtonListener(); - //videoView.restart(); - videoControls.setRewindButtonRemoved(true); - videoControls.setFastForwardButtonRemoved(true); - isFinished = true; - videoControls.getSeekBar().setEnabled(false); - } - - @Override - protected void onPause() { - super.onPause(); - videoView.stopPlayback(); - lastPosition = videoView.getCurrentPosition(); + protected void onStop() { + super.onStop(); + if (DEBUG) Log.d(TAG, "onStop() called"); + activityPaused = true; + playerImpl.destroy(); + playerImpl.setVideoStartPos((int) playerImpl.getPlayer().getCurrentPosition()); } @Override protected void onResume() { super.onResume(); - if (lastPosition > 0) videoView.seekTo(lastPosition); + if (DEBUG) Log.d(TAG, "onResume() called"); + if (activityPaused) { + //playerImpl.getPlayer().setPlayWhenReady(true); + playerImpl.getPlayPauseButton().setImageResource(R.drawable.ic_play_arrow_white); + playerImpl.initPlayer(); + playerImpl.playVideo(playerImpl.getSelectedStreamUri(), false); + activityPaused = false; + } } @Override protected void onDestroy() { super.onDestroy(); - videoView.stopPlayback(); + if (DEBUG) Log.d(TAG, "onDestroy() called"); + if (playerImpl != null) playerImpl.destroy(); + if (broadcastReceiver != null) unregisterReceiver(broadcastReceiver); } + /*////////////////////////////////////////////////////////////////////////// + // Init + //////////////////////////////////////////////////////////////////////////*/ + + private void initReceiver() { + if (DEBUG) Log.d(TAG, "initReceiver() called"); + broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (DEBUG) Log.d(TAG, "onReceive() called with: context = [" + context + "], intent = [" + intent + "]"); + switch (intent.getAction()) { + case AbstractPlayer.ACTION_UPDATE_THUMB: + playerImpl.onUpdateThumbnail(intent); + break; + } + } + }; + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(AbstractPlayer.ACTION_UPDATE_THUMB); + registerReceiver(broadcastReceiver, intentFilter); + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + private void showSystemUi() { if (DEBUG) Log.d(TAG, "showSystemUi() called"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + getWindow().getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + ); + } else getWindow().getDecorView().setSystemUiVisibility(0); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - getWindow().getDecorView().setSystemUiVisibility(0); } private void hideSystemUi() { if (DEBUG) Log.d(TAG, "hideSystemUi() called"); - if (android.os.Build.VERSION.SDK_INT >= 17) { - getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + if (android.os.Build.VERSION.SDK_INT >= 16) { + int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_FULLSCREEN - | View.SYSTEM_UI_FLAG_IMMERSIVE - | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) visibility |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + getWindow().getDecorView().setSystemUiVisibility(visibility); } getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); } - private class CustomVideoControls extends VideoControlsMobile { - protected static final int FAST_FORWARD_REWIND_AMOUNT = 8000; + /////////////////////////////////////////////////////////////////////////// - protected ImageButton fastForwardButton; - protected ImageButton rewindButton; + @SuppressWarnings({"unused", "WeakerAccess"}) + private class AbstractPlayerImpl extends AbstractPlayer { + private TextView titleTextView; + private TextView channelTextView; + private TextView volumeTextView; + private TextView brightnessTextView; + private ImageButton repeatButton; - public CustomVideoControls(Context context) { - super(context); + private ImageButton playPauseButton; + + AbstractPlayerImpl() { + super("AbstractPlayerImpl" + ExoPlayerActivity.TAG, ExoPlayerActivity.this); } @Override - protected int getLayoutResource() { - return R.layout.exomedia_custom_controls; + public void initViews(View rootView) { + super.initViews(rootView); + this.titleTextView = (TextView) rootView.findViewById(R.id.titleTextView); + this.channelTextView = (TextView) rootView.findViewById(R.id.channelTextView); + this.volumeTextView = (TextView) rootView.findViewById(R.id.volumeTextView); + this.brightnessTextView = (TextView) rootView.findViewById(R.id.brightnessTextView); + this.repeatButton = (ImageButton) rootView.findViewById(R.id.repeatButton); + + this.playPauseButton = (ImageButton) rootView.findViewById(R.id.playPauseButton); + + // Due to a bug on lower API, lets set the alpha instead of using a drawable + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) repeatButton.setImageAlpha(77); + else { //noinspection deprecation + repeatButton.setAlpha(77); + } + } @Override - protected void retrieveViews() { - super.retrieveViews(); - rewindButton = (ImageButton) findViewById(R.id.exomedia_controls_frewind_btn); - fastForwardButton = (ImageButton) findViewById(R.id.exomedia_controls_fforward_btn); + public void initListeners() { + super.initListeners(); + + MySimpleOnGestureListener listener = new MySimpleOnGestureListener(); + gestureDetector = new GestureDetector(context, listener); + gestureDetector.setIsLongpressEnabled(false); + playerImpl.getRootView().setOnTouchListener(listener); + + repeatButton.setOnClickListener(this); + playPauseButton.setOnClickListener(this); } @Override - protected void registerListeners() { - super.registerListeners(); - rewindButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - onRewindClicked(); - } - }); - fastForwardButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - onFastForwardClicked(); - } - }); - } - - public boolean onFastForwardClicked() { - if (videoView == null) return false; - - int newPosition = videoView.getCurrentPosition() + FAST_FORWARD_REWIND_AMOUNT; - if (newPosition > seekBar.getMax()) newPosition = seekBar.getMax(); - - performSeek(newPosition); - return true; - } - - public boolean onRewindClicked() { - if (videoView == null) return false; - - int newPosition = videoView.getCurrentPosition() - FAST_FORWARD_REWIND_AMOUNT; - if (newPosition < 0) newPosition = 0; - - performSeek(newPosition); - return true; + public void handleIntent(Intent intent) { + super.handleIntent(intent); + titleTextView.setText(getVideoTitle()); + channelTextView.setText(getChannelName()); } @Override - public void setFastForwardButtonRemoved(boolean removed) { - fastForwardButton.setVisibility(removed ? View.GONE : View.VISIBLE); + public void playVideo(Uri videoURI, boolean autoPlay) { + super.playVideo(videoURI, autoPlay); + playPauseButton.setImageResource(autoPlay ? R.drawable.ic_pause_white : R.drawable.ic_play_arrow_white); } @Override - public void setRewindButtonRemoved(boolean removed) { - rewindButton.setVisibility(removed ? View.GONE : View.VISIBLE); + public void onFullScreenButtonClicked() { + if (DEBUG) Log.d(TAG, "onFullScreenButtonClicked() called"); + if (playerImpl.getPlayer() == null) return; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && !PermissionHelper.checkSystemAlertWindowPermission(ExoPlayerActivity.this)) { + Toast.makeText(ExoPlayerActivity.this, R.string.msg_popup_permission, Toast.LENGTH_LONG).show(); + return; + } + + Intent i = new Intent(ExoPlayerActivity.this, PopupVideoPlayer.class); + i.putExtra(AbstractPlayer.VIDEO_TITLE, getVideoTitle()) + .putExtra(AbstractPlayer.CHANNEL_NAME, getChannelName()) + .putExtra(AbstractPlayer.VIDEO_URL, getVideoUrl()) + .putExtra(AbstractPlayer.INDEX_SEL_VIDEO_STREAM, getSelectedIndexStream()) + .putExtra(AbstractPlayer.VIDEO_STREAMS_LIST, getVideoStreamsList()) + .putExtra(AbstractPlayer.START_POSITION, ((int) getPlayer().getCurrentPosition())); + context.startService(i); + ((View) getControlAnimationView().getParent()).setVisibility(View.GONE); + if (playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(false); + ExoPlayerActivity.this.finish(); } @Override - protected void onPlayPauseClick() { - super.onPlayPauseClick(); - if (videoView == null) return; - if (DEBUG) Log.d(TAG, "onPlayPauseClick() called" + videoView.getDuration() + " position= " + videoView.getCurrentPosition()); - if (isFinished) { - videoView.restart(); - setRewindButtonRemoved(false); - setFastForwardButtonRemoved(false); - isFinished = false; - seekBar.setEnabled(true); + @SuppressWarnings("deprecation") + public void onRepeatClicked() { + super.onRepeatClicked(); + if (DEBUG) Log.d(TAG, "onRepeatClicked() called"); + switch (getCurrentRepeatMode()) { + case REPEAT_DISABLED: + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) repeatButton.setImageAlpha(77); + else repeatButton.setAlpha(77); + + break; + case REPEAT_ONE: + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) repeatButton.setImageAlpha(255); + else repeatButton.setAlpha(255); + + break; + case REPEAT_ALL: + // Waiting :) + break; } } - private void performSeek(int newPosition) { - internalListener.onSeekEnded(newPosition); + @Override + public void onClick(View v) { + super.onClick(v); + if (v.getId() == repeatButton.getId()) onRepeatClicked(); + else if (v.getId() == playPauseButton.getId()) onVideoPlayPause(); + + if (getCurrentState() != STATE_COMPLETED) { + animateView(playerImpl.getControlsRoot(), true, 300, 0, new Runnable() { + @Override + public void run() { + if (getCurrentState() == STATE_PLAYING && !playerImpl.isQualityMenuVisible()) { + animateView(playerImpl.getControlsRoot(), false, 300, DEFAULT_CONTROLS_HIDE_TIME, true); + } + } + }, false); + } } - public SeekBar getSeekBar() { - return seekBar; + @Override + public void onVideoPlayPause() { + super.onVideoPlayPause(); + if (getPlayer().getPlayWhenReady()) { + animateView(playPauseButton, false, 80, 0, new Runnable() { + @Override + public void run() { + playPauseButton.setImageResource(R.drawable.ic_pause_white); + animateView(playPauseButton, true, 200, 0); + } + }); + } else { + animateView(playPauseButton, false, 80, 0, new Runnable() { + @Override + public void run() { + playPauseButton.setImageResource(R.drawable.ic_play_arrow_white); + animateView(playPauseButton, true, 200, 0); + } + }); + } + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + super.onStopTrackingTouch(seekBar); + if (playerImpl.wasPlaying()) { + hideSystemUi(); + playerImpl.getControlsRoot().setVisibility(View.GONE); + } + } + + @Override + public void onDismiss(PopupMenu menu) { + super.onDismiss(menu); + if (isPlaying()) animateView(getControlsRoot(), false, 500, 0, true); + } + + @Override + public void onError() { + Toast.makeText(context, "Failed to play this video", Toast.LENGTH_SHORT).show(); + finish(); + } + + /*////////////////////////////////////////////////////////////////////////// + // States + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onLoading() { + super.onLoading(); + hideSystemUi(); + playPauseButton.setImageResource(R.drawable.ic_pause_white); + } + + @Override + public void onPaused() { + super.onPaused(); + animateView(playPauseButton, true, 100, 0); + showSystemUi(); + } + + @Override + public void onPausedSeek() { + super.onPausedSeek(); + animateView(playPauseButton, false, 100, 0); + } + + @Override + public void onPlaying() { + super.onPlaying(); + animateView(playPauseButton, true, 500, 0); + } + + @Override + public void onCompleted() { + if (getCurrentRepeatMode() == RepeatMode.REPEAT_ONE) { + playPauseButton.setImageResource(R.drawable.ic_pause_white); + } else { + showSystemUi(); + animateView(playPauseButton, false, 0, 0, new Runnable() { + @Override + public void run() { + playPauseButton.setImageResource(R.drawable.ic_replay_white); + animateView(playPauseButton, true, 300, 0); + } + }); + } + super.onCompleted(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void animateView(View view, boolean enterOrExit, long duration, long delay, final Runnable execOnEnd, boolean hideUi) { + //if (execOnEnd == null) playerImpl.setDefaultAnimationEnd(hideUiRunnable); + + if (hideUi && execOnEnd != null) { + Runnable combinedRunnable = new Runnable() { + @Override + public void run() { + execOnEnd.run(); + hideUiRunnable.run(); + } + }; + super.animateView(view, enterOrExit, duration, delay, combinedRunnable, true); + } else super.animateView(view, enterOrExit, duration, delay, hideUi ? hideUiRunnable : execOnEnd, hideUi); + } + + /////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////// + + public TextView getTitleTextView() { + return titleTextView; + } + + public TextView getChannelTextView() { + return channelTextView; + } + + public TextView getVolumeTextView() { + return volumeTextView; + } + + public TextView getBrightnessTextView() { + return brightnessTextView; + } + + public ImageButton getRepeatButton() { + return repeatButton; + } + + public ImageButton getPlayPauseButton() { + return playPauseButton; } } -} + + private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener { + private boolean isMoving; + + @Override + public boolean onDoubleTap(MotionEvent e) { + if (DEBUG) Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY()); + if (!playerImpl.isPlaying()) return false; + if (e.getX() > playerImpl.getRootView().getWidth() / 2) playerImpl.onFastForward(); + else playerImpl.onFastRewind(); + return true; + } + + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); + if (playerImpl.getCurrentState() != StateInterface.STATE_PLAYING) return true; + + if (playerImpl.isControlsVisible()) playerImpl.animateView(playerImpl.getControlsRoot(), false, 150, 0, true); + else { + playerImpl.animateView(playerImpl.getControlsRoot(), true, 500, 0, new Runnable() { + @Override + public void run() { + playerImpl.animateView(playerImpl.getControlsRoot(), false, 300, AbstractPlayer.DEFAULT_CONTROLS_HIDE_TIME, true); + } + }); + showSystemUi(); + } + return true; + } + + private final float stepsBrightness = 21, stepBrightness = (1f / stepsBrightness), minBrightness = .01f; + private float currentBrightness = .5f; + + private int currentVolume, maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); + + private final String brightnessUnicode = new String(Character.toChars(0x2600)); + // private final String volumeUnicode = new String(Character.toChars(0x1F50A)); + private final String volumeUnicode = new String(Character.toChars(0x1F508)); + + + private final int MOVEMENT_THRESHOLD = 40; + private final int eventsThreshold = 3; + private boolean triggered = false; + private int eventsNum; + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + //noinspection PointlessBooleanExpression + if (DEBUG && false) Log.d(TAG, "ExoPlayerActivity.onScroll = " + + ", e1.getRaw = [" + e1.getRawX() + ", " + e1.getRawY() + "]" + + ", e2.getRaw = [" + e2.getRawX() + ", " + e2.getRawY() + "]" + + ", distanceXy = [" + distanceX + ", " + distanceY + "]"); + float abs = Math.abs(e2.getY() - e1.getY()); + if (!triggered) { + triggered = abs > MOVEMENT_THRESHOLD; + return false; + } + + if (eventsNum++ % eventsThreshold != 0 || playerImpl.getCurrentState() == StateInterface.STATE_COMPLETED) return false; + isMoving = true; +// boolean up = !((e2.getY() - e1.getY()) > 0) && distanceY > 0; // Android's origin point is on top + boolean up = distanceY > 0; + + + if (e1.getX() > playerImpl.getRootView().getWidth() / 2) { + currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) + (up ? 1 : -1); + if (currentVolume >= maxVolume) currentVolume = maxVolume; + if (currentVolume <= 0) currentVolume = 0; + audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, currentVolume, 0); + + if (DEBUG) Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume); + playerImpl.getVolumeTextView().setText(volumeUnicode + " " + Math.round((((float) currentVolume) / maxVolume) * 100) + "%"); + + if (playerImpl.getVolumeTextView().getVisibility() != View.VISIBLE) playerImpl.animateView(playerImpl.getVolumeTextView(), true, 200, 0); + if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) playerImpl.getBrightnessTextView().setVisibility(View.GONE); + } else { + WindowManager.LayoutParams lp = getWindow().getAttributes(); + currentBrightness += up ? stepBrightness : -stepBrightness; + if (currentBrightness >= 1f) currentBrightness = 1f; + if (currentBrightness <= minBrightness) currentBrightness = minBrightness; + + lp.screenBrightness = currentBrightness; + getWindow().setAttributes(lp); + if (DEBUG) Log.d(TAG, "onScroll().brightnessControl, currentBrightness = " + currentBrightness); + int brightnessNormalized = Math.round(currentBrightness * 100); + + playerImpl.getBrightnessTextView().setText(brightnessUnicode + " " + (brightnessNormalized == 1 ? 0 : brightnessNormalized) + "%"); + + if (playerImpl.getBrightnessTextView().getVisibility() != View.VISIBLE) playerImpl.animateView(playerImpl.getBrightnessTextView(), true, 200, 0); + if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) playerImpl.getVolumeTextView().setVisibility(View.GONE); + } + return true; + } + + private void onScrollEnd() { + if (DEBUG) Log.d(TAG, "onScrollEnd() called"); + triggered = false; + eventsNum = 0; + /* if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) playerImpl.getVolumeTextView().setVisibility(View.GONE); + if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) playerImpl.getBrightnessTextView().setVisibility(View.GONE);*/ + if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) playerImpl.animateView(playerImpl.getVolumeTextView(), false, 200, 200); + if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) playerImpl.animateView(playerImpl.getBrightnessTextView(), false, 200, 200); + + if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == StateInterface.STATE_PLAYING) { + playerImpl.animateView(playerImpl.getControlsRoot(), false, 300, AbstractPlayer.DEFAULT_CONTROLS_HIDE_TIME); + } + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + gestureDetector.onTouchEvent(event); + if (event.getAction() == MotionEvent.ACTION_UP && isMoving) { + isMoving = false; + onScrollEnd(); + } + return true; + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayVideoActivity.java b/app/src/main/java/org/schabi/newpipe/player/PlayVideoActivity.java index f13e73b49..49da537ac 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayVideoActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayVideoActivity.java @@ -5,9 +5,8 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.res.Configuration; -import android.graphics.drawable.Drawable; -import android.media.MediaPlayer; import android.media.AudioManager; +import android.media.MediaPlayer; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -28,7 +27,6 @@ import android.widget.MediaController; import android.widget.ProgressBar; import android.widget.VideoView; -import org.schabi.newpipe.App; import org.schabi.newpipe.R; /** 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 e396ac1b6..661cb1632 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -1,10 +1,5 @@ package org.schabi.newpipe.player; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.animation.PropertyValuesHolder; -import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.app.NotificationManager; import android.app.PendingIntent; @@ -13,17 +8,15 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.SharedPreferences; import android.content.res.Configuration; +import android.content.res.Resources; import android.graphics.Bitmap; -import android.graphics.Color; import android.graphics.PixelFormat; import android.net.Uri; +import android.os.Build; import android.os.Handler; import android.os.IBinder; -import android.preference.PreferenceManager; import android.support.v4.app.NotificationCompat; -import android.support.v4.content.ContextCompat; import android.util.DisplayMetrics; import android.util.Log; import android.view.GestureDetector; @@ -31,17 +24,10 @@ import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; +import android.widget.PopupMenu; import android.widget.RemoteViews; -import android.widget.SeekBar; import android.widget.Toast; -import com.devbrackets.android.exomedia.listener.OnCompletionListener; -import com.devbrackets.android.exomedia.listener.OnErrorListener; -import com.devbrackets.android.exomedia.listener.OnPreparedListener; -import com.devbrackets.android.exomedia.listener.OnSeekCompletionListener; -import com.devbrackets.android.exomedia.ui.widget.EMVideoView; -import com.devbrackets.android.exomedia.util.Repeater; -import com.devbrackets.android.exomedia.util.TimeFormatUtil; import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; @@ -56,210 +42,204 @@ import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.stream_info.StreamExtractor; import org.schabi.newpipe.extractor.stream_info.StreamInfo; import org.schabi.newpipe.extractor.stream_info.VideoStream; -import org.schabi.newpipe.player.popup.PopupViewHolder; -import org.schabi.newpipe.player.popup.StateInterface; import org.schabi.newpipe.util.NavStack; +import org.schabi.newpipe.util.ThemeHelper; import java.io.IOException; +import java.util.ArrayList; -public class PopupVideoPlayer extends Service implements StateInterface { +/** + * Service Popup Player implementing AbstractPlayer + * + * @author mauriciocolli + */ +public class PopupVideoPlayer extends Service { private static final String TAG = ".PopupVideoPlayer"; - private static final boolean DEBUG = false; - private static int CURRENT_STATE = -1; + private static final boolean DEBUG = AbstractPlayer.DEBUG; private static final int NOTIFICATION_ID = 40028922; - protected static final int FAST_FORWARD_REWIND_AMOUNT = 10000; // 10 Seconds - protected static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds + public static final String ACTION_CLOSE = "org.schabi.newpipe.player.PopupVideoPlayer.CLOSE"; + public static final String ACTION_PLAY_PAUSE = "org.schabi.newpipe.player.PopupVideoPlayer.PLAY_PAUSE"; + public static final String ACTION_OPEN_DETAIL = "org.schabi.newpipe.player.PopupVideoPlayer.OPEN_DETAIL"; + public static final String ACTION_REPEAT = "org.schabi.newpipe.player.PopupVideoPlayer.REPEAT"; private BroadcastReceiver broadcastReceiver; - private InternalListener internalListener; private WindowManager windowManager; private WindowManager.LayoutParams windowLayoutParams; private GestureDetector gestureDetector; - private ValueAnimator controlViewAnimator; - private PopupViewHolder viewHolder; - private EMVideoView emVideoView; private float screenWidth, screenHeight; private float popupWidth, popupHeight; - private float currentPopupHeight = 200; + private float currentPopupHeight = 110.0f * Resources.getSystem().getDisplayMetrics().density; //private float minimumHeight = 100; // TODO: Use it when implementing the resize of the popup - public static final String VIDEO_URL = "video_url"; - public static final String STREAM_URL = "stream_url"; - public static final String VIDEO_TITLE = "video_title"; - public static final String CHANNEL_NAME = "channel_name"; - + private final String setAlphaMethodName = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) ? "setImageAlpha" : "setAlpha"; private NotificationManager notificationManager; private NotificationCompat.Builder notBuilder; private RemoteViews notRemoteView; - private Uri streamUri; - private String videoUrl = ""; - private String videoTitle = ""; - private volatile String channelName = ""; private ImageLoader imageLoader = ImageLoader.getInstance(); - private DisplayImageOptions displayImageOptions = - new DisplayImageOptions.Builder().cacheInMemory(true).build(); - private volatile Bitmap videoThumbnail; + private DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder().cacheInMemory(true).build(); - private Repeater progressPollRepeater = new Repeater(); - private SharedPreferences sharedPreferences; + private AbstractPlayerImpl playerImpl; + + /*////////////////////////////////////////////////////////////////////////// + // Service LifeCycle + //////////////////////////////////////////////////////////////////////////*/ @Override public void onCreate() { windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)); - internalListener = new InternalListener(); - viewHolder = new PopupViewHolder(null); - progressPollRepeater.setRepeatListener(internalListener); - progressPollRepeater.setRepeaterDelay(500); - sharedPreferences = PreferenceManager.getDefaultSharedPreferences(PopupVideoPlayer.this); initReceiver(); + + playerImpl = new AbstractPlayerImpl(); + ThemeHelper.setTheme(this, false); } + @Override + @SuppressWarnings("unchecked") + public int onStartCommand(final Intent intent, int flags, int startId) { + if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]"); + if (playerImpl.getPlayer() == null) initPopup(); + if (!playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(true); + + if (imageLoader != null) imageLoader.clearMemoryCache(); + if (intent.getStringExtra(NavStack.URL) != null) { + playerImpl.setStartedFromNewPipe(false); + Thread fetcher = new Thread(new FetcherRunnable(intent)); + fetcher.start(); + } else { + playerImpl.setStartedFromNewPipe(true); + playerImpl.handleIntent(intent); + } + return START_NOT_STICKY; + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + updateScreenSize(); + } + + @Override + public void onDestroy() { + if (DEBUG) Log.d(TAG, "onDestroy() called"); + stopForeground(true); + if (playerImpl != null) { + playerImpl.destroy(); + if (playerImpl.getRootView() != null) windowManager.removeView(playerImpl.getRootView()); + } + if (imageLoader != null) imageLoader.clearMemoryCache(); + if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID); + if (broadcastReceiver != null) unregisterReceiver(broadcastReceiver); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + /*////////////////////////////////////////////////////////////////////////// + // Init + //////////////////////////////////////////////////////////////////////////*/ + private void initReceiver() { broadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - if (DEBUG) - Log.d(TAG, "onReceive() called with: context = [" + context + "], intent = [" + intent + "]"); + if (DEBUG) Log.d(TAG, "onReceive() called with: context = [" + context + "], intent = [" + intent + "]"); switch (intent.getAction()) { - case InternalListener.ACTION_CLOSE: - internalListener.onVideoClose(); + case ACTION_CLOSE: + onVideoClose(); break; - case InternalListener.ACTION_PLAY_PAUSE: - internalListener.onVideoPlayPause(); + case ACTION_PLAY_PAUSE: + playerImpl.onVideoPlayPause(); break; - case InternalListener.ACTION_OPEN_DETAIL: - internalListener.onOpenDetail(PopupVideoPlayer.this, videoUrl); + case ACTION_OPEN_DETAIL: + onOpenDetail(PopupVideoPlayer.this, playerImpl.getVideoUrl()); break; - case InternalListener.ACTION_UPDATE_THUMB: - internalListener.onUpdateThumbnail(intent); + case ACTION_REPEAT: + playerImpl.onRepeatClicked(); + break; + case AbstractPlayer.ACTION_UPDATE_THUMB: + playerImpl.onUpdateThumbnail(intent); break; } } }; IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(InternalListener.ACTION_CLOSE); - intentFilter.addAction(InternalListener.ACTION_PLAY_PAUSE); - intentFilter.addAction(InternalListener.ACTION_OPEN_DETAIL); - intentFilter.addAction(InternalListener.ACTION_UPDATE_THUMB); + intentFilter.addAction(ACTION_CLOSE); + intentFilter.addAction(ACTION_PLAY_PAUSE); + intentFilter.addAction(ACTION_OPEN_DETAIL); + intentFilter.addAction(ACTION_REPEAT); + intentFilter.addAction(AbstractPlayer.ACTION_UPDATE_THUMB); registerReceiver(broadcastReceiver, intentFilter); } - @SuppressLint({"RtlHardcoded"}) + @SuppressLint("RtlHardcoded") private void initPopup() { if (DEBUG) Log.d(TAG, "initPopup() called"); View rootView = View.inflate(this, R.layout.player_popup, null); - viewHolder = new PopupViewHolder(rootView); - viewHolder.getPlaybackSeekBar().setOnSeekBarChangeListener(internalListener); - emVideoView = viewHolder.getVideoView(); - emVideoView.setOnPreparedListener(internalListener); - emVideoView.setOnCompletionListener(internalListener); - emVideoView.setOnErrorListener(internalListener); - emVideoView.setOnSeekCompletionListener(internalListener); + playerImpl.setup(rootView); + + updateScreenSize(); windowLayoutParams = new WindowManager.LayoutParams( (int) getMinimumVideoWidth(currentPopupHeight), (int) currentPopupHeight, WindowManager.LayoutParams.TYPE_PHONE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT); + windowLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; MySimpleOnGestureListener listener = new MySimpleOnGestureListener(); gestureDetector = new GestureDetector(this, listener); gestureDetector.setIsLongpressEnabled(false); rootView.setOnTouchListener(listener); - updateScreenSize(); - + playerImpl.getLoadingPanel().setMinimumWidth(windowLayoutParams.width); + playerImpl.getLoadingPanel().setMinimumHeight(windowLayoutParams.height); windowManager.addView(rootView, windowLayoutParams); } - @Override - public int onStartCommand(final Intent intent, int flags, int startId) { - if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]"); - if (emVideoView == null) initPopup(); - - if (intent.getStringExtra(NavStack.URL) != null) { - Thread fetcher = new Thread(new FetcherRunnable(intent)); - fetcher.start(); - } else { - if (imageLoader != null) imageLoader.clearMemoryCache(); - streamUri = Uri.parse(intent.getStringExtra(STREAM_URL)); - videoUrl = intent.getStringExtra(VIDEO_URL); - videoTitle = intent.getStringExtra(VIDEO_TITLE); - channelName = intent.getStringExtra(CHANNEL_NAME); - try { - videoThumbnail = ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail; - } catch (Exception e) { - e.printStackTrace(); - } - playVideo(streamUri); - } - return START_NOT_STICKY; - } - - private float getMinimumVideoWidth(float height) { - float width = height * (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have - if (DEBUG) Log.d(TAG, "getMinimumVideoWidth() called with: height = [" + height + "], returned: " + width); - return width; - } - - private void updateScreenSize() { - DisplayMetrics metrics = new DisplayMetrics(); - windowManager.getDefaultDisplay().getMetrics(metrics); - - screenWidth = metrics.widthPixels; - screenHeight = metrics.heightPixels; - if (DEBUG) Log.d(TAG, "updateScreenSize() called > screenWidth = " + screenWidth + ", screenHeight = " + screenHeight); - } - - private void seekBy(int milliSeconds) { - if (emVideoView == null) return; - int progress = emVideoView.getCurrentPosition() + milliSeconds; - emVideoView.seekTo(progress); - } - - private void playVideo(Uri videoURI) { - if (DEBUG) Log.d(TAG, "playVideo() called with: streamUri = [" + streamUri + "]"); - - changeState(STATE_LOADING); - - windowLayoutParams.width = (int) getMinimumVideoWidth(currentPopupHeight); - windowManager.updateViewLayout(viewHolder.getRootView(), windowLayoutParams); - - if (videoURI == null || emVideoView == null || viewHolder.getRootView() == null) { - Toast.makeText(this, "Failed to play this video", Toast.LENGTH_SHORT).show(); - stopSelf(); - return; - } - if (emVideoView.isPlaying()) emVideoView.stopPlayback(); - emVideoView.setVideoURI(videoURI); - - notBuilder = createNotification(); - startForeground(NOTIFICATION_ID, notBuilder.build()); - notificationManager.notify(NOTIFICATION_ID, this.notBuilder.build()); - } + /*////////////////////////////////////////////////////////////////////////// + // Notification + //////////////////////////////////////////////////////////////////////////*/ private NotificationCompat.Builder createNotification() { notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_popup_notification); - if (videoThumbnail != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, videoThumbnail); + + if (playerImpl.getVideoThumbnail() != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, playerImpl.getVideoThumbnail()); else notRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail); + notRemoteView.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle()); + notRemoteView.setTextViewText(R.id.notificationArtist, playerImpl.getChannelName()); + notRemoteView.setOnClickPendingIntent(R.id.notificationPlayPause, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(InternalListener.ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT)); + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT)); notRemoteView.setOnClickPendingIntent(R.id.notificationStop, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(InternalListener.ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT)); - notRemoteView.setTextViewText(R.id.notificationSongName, videoTitle); - notRemoteView.setTextViewText(R.id.notificationArtist, channelName); + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT)); notRemoteView.setOnClickPendingIntent(R.id.notificationContent, - PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(InternalListener.ACTION_OPEN_DETAIL), PendingIntent.FLAG_UPDATE_CURRENT)); + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_OPEN_DETAIL), PendingIntent.FLAG_UPDATE_CURRENT)); + notRemoteView.setOnClickPendingIntent(R.id.notificationRepeat, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT)); + + switch (playerImpl.getCurrentRepeatMode()) { + case REPEAT_DISABLED: + notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 77); + break; + case REPEAT_ONE: + notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 255); + break; + case REPEAT_ALL: + // Waiting :) + break; + } return new NotificationCompat.Builder(this) .setOngoing(true) - .setSmallIcon(R.drawable.ic_play_arrow_white_48dp) + .setSmallIcon(R.drawable.ic_play_arrow_white) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setContent(notRemoteView); } @@ -276,384 +256,175 @@ public class PopupVideoPlayer extends Service implements StateInterface { notificationManager.notify(NOTIFICATION_ID, notBuilder.build()); } - /** - * Show a animation, and depending on goneOnEnd, will stay on the screen or be gone - * - * @param drawableId the drawable that will be used to animate, pass -1 to clear any animation that is visible - * @param goneOnEnd will set the animation view to GONE on the end of the animation - */ - private void showAndAnimateControl(final int drawableId, final boolean goneOnEnd) { - if (DEBUG) Log.d(TAG, "showAndAnimateControl() called with: drawableId = [" + drawableId + "], goneOnEnd = [" + goneOnEnd + "]"); - if (controlViewAnimator != null && controlViewAnimator.isRunning()) { - if (DEBUG) Log.d(TAG, "showAndAnimateControl: controlViewAnimator.isRunning"); - controlViewAnimator.end(); - } - if (drawableId == -1) { - if (viewHolder.getControlAnimationView().getVisibility() == View.VISIBLE) { - controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(viewHolder.getControlAnimationView(), - PropertyValuesHolder.ofFloat(View.ALPHA, 1f, 0f), - PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1f), - PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1f) - ).setDuration(300); - controlViewAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - viewHolder.getControlAnimationView().setVisibility(View.GONE); - } - }); - controlViewAnimator.start(); - } - return; - } + /*////////////////////////////////////////////////////////////////////////// + // Misc + //////////////////////////////////////////////////////////////////////////*/ - float scaleFrom = goneOnEnd ? 1f : 1f, scaleTo = goneOnEnd ? 1.8f : 1.4f; - float alphaFrom = goneOnEnd ? 1f : 0f, alphaTo = goneOnEnd ? 0f : 1f; - - - controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(viewHolder.getControlAnimationView(), - PropertyValuesHolder.ofFloat(View.ALPHA, alphaFrom, alphaTo), - PropertyValuesHolder.ofFloat(View.SCALE_X, scaleFrom, scaleTo), - PropertyValuesHolder.ofFloat(View.SCALE_Y, scaleFrom, scaleTo) - ); - controlViewAnimator.setDuration(goneOnEnd ? 1000 : 500); - controlViewAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - if (goneOnEnd) viewHolder.getControlAnimationView().setVisibility(View.GONE); - else viewHolder.getControlAnimationView().setVisibility(View.VISIBLE); - } - }); - - - viewHolder.getControlAnimationView().setVisibility(View.VISIBLE); - viewHolder.getControlAnimationView().setImageDrawable(ContextCompat.getDrawable(PopupVideoPlayer.this, drawableId)); - controlViewAnimator.start(); + public void onVideoClose() { + if (DEBUG) Log.d(TAG, "onVideoClose() called"); + stopSelf(); } - /** - * Animate the view - * - * @param enterOrExit true to enter, false to exit - * @param duration how long the animation will take, in milliseconds - * @param delay how long the animation will wait to start, in milliseconds - */ - private void animateView(final View view, final boolean enterOrExit, long duration, long delay) { - if (DEBUG) Log.d(TAG, "animateView() called with: view = [" + view + "], enterOrExit = [" + enterOrExit + "], duration = [" + duration + "], delay = [" + delay + "]"); - if (view.getVisibility() == View.VISIBLE && enterOrExit) { - if (DEBUG) Log.d(TAG, "animateLoadingPanel() > view.getVisibility() == View.VISIBLE && enterOrExit"); - view.animate().setListener(null).cancel(); - view.setVisibility(View.VISIBLE); - return; - } - - view.animate().setListener(null).cancel(); - view.setVisibility(View.VISIBLE); - - if (view == viewHolder.getControlsRoot()) { - if (enterOrExit) { - view.setAlpha(0f); - view.animate().alpha(1f).setDuration(duration).setStartDelay(delay).setListener(null).start(); - } else { - view.setAlpha(1f); - view.animate().alpha(0f) - .setDuration(duration).setStartDelay(delay) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - view.setVisibility(View.GONE); - } - }) - .start(); - } - return; - } - - if (enterOrExit) { - view.setAlpha(0f); - view.setScaleX(.8f); - view.setScaleY(.8f); - view.animate().alpha(1f).scaleX(1f).scaleY(1f).setDuration(duration).setStartDelay(delay).setListener(null).start(); - } else { - view.setAlpha(1f); - view.setScaleX(1f); - view.setScaleY(1f); - view.animate().alpha(0f).scaleX(.8f).scaleY(.8f).setDuration(duration).setStartDelay(delay) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - view.setVisibility(View.GONE); - } - }) - .start(); - } + public void onOpenDetail(Context context, String videoUrl) { + if (DEBUG) Log.d(TAG, "onOpenDetail() called with: context = [" + context + "], videoUrl = [" + videoUrl + "]"); + Intent i = new Intent(context, VideoItemDetailActivity.class); + i.putExtra(NavStack.SERVICE_ID, 0) + .putExtra(NavStack.URL, videoUrl) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(i); + //NavStack.getInstance().openDetailActivity(context, videoUrl, 0); } - @Override - public void onConfigurationChanged(Configuration newConfig) { - updateScreenSize(); + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + private float getMinimumVideoWidth(float height) { + float width = height * (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have + if (DEBUG) Log.d(TAG, "getMinimumVideoWidth() called with: height = [" + height + "], returned: " + width); + return width; } - @Override - public void onDestroy() { - if (DEBUG) Log.d(TAG, "onDestroy() called"); - stopForeground(true); - if (emVideoView != null) emVideoView.stopPlayback(); - if (imageLoader != null) imageLoader.clearMemoryCache(); - if (viewHolder.getRootView() != null) windowManager.removeView(viewHolder.getRootView()); - if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID); - if (progressPollRepeater != null) { - progressPollRepeater.stop(); - progressPollRepeater.setRepeatListener(null); - } - if (broadcastReceiver != null) unregisterReceiver(broadcastReceiver); - } + private void updateScreenSize() { + DisplayMetrics metrics = new DisplayMetrics(); + windowManager.getDefaultDisplay().getMetrics(metrics); - @Override - public IBinder onBind(Intent intent) { - return null; + screenWidth = metrics.widthPixels; + screenHeight = metrics.heightPixels; + if (DEBUG) Log.d(TAG, "updateScreenSize() called > screenWidth = " + screenWidth + ", screenHeight = " + screenHeight); } - /////////////////////////////////////////////////////////////////////////// - // States Implementation /////////////////////////////////////////////////////////////////////////// - @Override - public void changeState(int state) { - if (DEBUG) Log.d(TAG, "changeState() called with: state = [" + state + "]"); - CURRENT_STATE = state; - switch (state) { - case STATE_LOADING: - onLoading(); - break; - case STATE_PLAYING: - onPlaying(); - break; - case STATE_PAUSED: - onPaused(); - break; - case STATE_PAUSED_SEEK: - onPausedSeek(); - break; - case STATE_COMPLETED: - onCompleted(); - break; + private class AbstractPlayerImpl extends AbstractPlayer { + AbstractPlayerImpl() { + super("AbstractPlayerImpl" + PopupVideoPlayer.TAG, PopupVideoPlayer.this); } - } - - @Override - public void onLoading() { - if (DEBUG) Log.d(TAG, "onLoading() called"); - updateNotification(R.drawable.ic_play_arrow_white_48dp); - - showAndAnimateControl(-1, true); - viewHolder.getPlaybackSeekBar().setEnabled(true); - viewHolder.getPlaybackSeekBar().setProgress(0); - viewHolder.getLoadingPanel().setBackgroundColor(Color.BLACK); - animateView(viewHolder.getLoadingPanel(), true, 500, 0); - viewHolder.getEndScreen().setVisibility(View.GONE); - viewHolder.getControlsRoot().setVisibility(View.GONE); - } - - @Override - public void onPlaying() { - if (DEBUG) Log.d(TAG, "onPlaying() called"); - updateNotification(R.drawable.ic_pause_white_24dp); - - showAndAnimateControl(-1, true); - viewHolder.getLoadingPanel().setVisibility(View.GONE); - animateView(viewHolder.getControlsRoot(), false, 500, DEFAULT_CONTROLS_HIDE_TIME); - } - - @Override - public void onPaused() { - if (DEBUG) Log.d(TAG, "onPaused() called"); - updateNotification(R.drawable.ic_play_arrow_white_48dp); - - showAndAnimateControl(R.drawable.ic_play_arrow_white_48dp, false); - animateView(viewHolder.getControlsRoot(), true, 500, 100); - viewHolder.getLoadingPanel().setVisibility(View.GONE); - } - - @Override - public void onPausedSeek() { - if (DEBUG) Log.d(TAG, "onPausedSeek() called"); - updateNotification(R.drawable.ic_play_arrow_white_48dp); - - showAndAnimateControl(-1, true); - viewHolder.getLoadingPanel().setBackgroundColor(Color.TRANSPARENT); - animateView(viewHolder.getLoadingPanel(), true, 300, 0); - } - - @Override - public void onCompleted() { - if (DEBUG) Log.d(TAG, "onCompleted() called"); - updateNotification(R.drawable.ic_replay_white); - showAndAnimateControl(R.drawable.ic_replay_white, false); - animateView(viewHolder.getControlsRoot(), true, 500, 0); - animateView(viewHolder.getEndScreen(), true, 200, 0); - viewHolder.getLoadingPanel().setVisibility(View.GONE); - viewHolder.getPlaybackSeekBar().setEnabled(false); - viewHolder.getPlaybackCurrentTime().setText(viewHolder.getPlaybackEndTime().getText()); - if (videoThumbnail != null) viewHolder.getEndScreen().setImageBitmap(videoThumbnail); - } - - /** - * This class joins all the necessary listeners - */ - @SuppressWarnings({"WeakerAccess"}) - public class InternalListener implements SeekBar.OnSeekBarChangeListener, OnPreparedListener, OnSeekCompletionListener, OnCompletionListener, OnErrorListener, Repeater.RepeatListener { - public static final String ACTION_CLOSE = "org.schabi.newpipe.player.PopupVideoPlayer.CLOSE"; - public static final String ACTION_PLAY_PAUSE = "org.schabi.newpipe.player.PopupVideoPlayer.PLAY_PAUSE"; - public static final String ACTION_OPEN_DETAIL = "org.schabi.newpipe.player.PopupVideoPlayer.OPEN_DETAIL"; - public static final String ACTION_UPDATE_THUMB = "org.schabi.newpipe.player.PopupVideoPlayer.UPDATE_THUMBNAIL"; @Override - public void onPrepared() { - if (DEBUG) Log.d(TAG, "onPrepared() called"); - viewHolder.getPlaybackSeekBar().setMax(emVideoView.getDuration()); - viewHolder.getPlaybackEndTime().setText(TimeFormatUtil.formatMs(emVideoView.getDuration())); + public void playVideo(Uri videoURI, boolean autoPlay) { + super.playVideo(videoURI, autoPlay); - changeState(STATE_PLAYING); - progressPollRepeater.start(); - emVideoView.start(); + windowLayoutParams.width = (int) getMinimumVideoWidth(currentPopupHeight); + windowManager.updateViewLayout(getRootView(), windowLayoutParams); + notBuilder = createNotification(); + startForeground(NOTIFICATION_ID, notBuilder.build()); + notificationManager.notify(NOTIFICATION_ID, notBuilder.build()); } - public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) { - if (viewHolder.isControlsVisible() && CURRENT_STATE != STATE_PAUSED_SEEK) { - viewHolder.getPlaybackSeekBar().setProgress(currentProgress); - viewHolder.getPlaybackCurrentTime().setText(TimeFormatUtil.formatMs(currentProgress)); - viewHolder.getPlaybackSeekBar().setSecondaryProgress((int) (viewHolder.getPlaybackSeekBar().getMax() * ((float) bufferPercent / 100))); + @Override + public void onFullScreenButtonClicked() { + if (DEBUG) Log.d(TAG, "onFullScreenButtonClicked() called"); + Intent intent; + //if (getSharedPreferences().getBoolean(getResources().getString(R.string.use_exoplayer_key), false)) { + // TODO: Remove this check when ExoPlayer is the default + // For now just disable the non-exoplayer player + //noinspection ConstantConditions,ConstantIfStatement + if (true) { + intent = new Intent(PopupVideoPlayer.this, ExoPlayerActivity.class) + .putExtra(AbstractPlayer.VIDEO_TITLE, getVideoTitle()) + .putExtra(AbstractPlayer.VIDEO_URL, getVideoUrl()) + .putExtra(AbstractPlayer.CHANNEL_NAME, getChannelName()) + .putExtra(AbstractPlayer.INDEX_SEL_VIDEO_STREAM, getSelectedIndexStream()) + .putExtra(AbstractPlayer.VIDEO_STREAMS_LIST, getVideoStreamsList()) + .putExtra(AbstractPlayer.START_POSITION, ((int) getPlayer().getCurrentPosition())); + if (!playerImpl.isStartedFromNewPipe()) intent.putExtra(AbstractPlayer.STARTED_FROM_NEWPIPE, false); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } else { + intent = new Intent(PopupVideoPlayer.this, PlayVideoActivity.class) + .putExtra(PlayVideoActivity.VIDEO_TITLE, getVideoTitle()) + .putExtra(PlayVideoActivity.STREAM_URL, getSelectedStreamUri().toString()) + .putExtra(PlayVideoActivity.VIDEO_URL, getVideoUrl()) + .putExtra(PlayVideoActivity.START_POSITION, Math.round(getPlayer().getCurrentPosition() / 1000f)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } - if (DEBUG && bufferPercent % 10 == 0) { //Limit log - Log.d(TAG, "updateProgress() called with: isVisible = " + viewHolder.isControlsVisible() + ", currentProgress = [" + currentProgress + "], duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]"); + context.startActivity(intent); + stopSelf(); + } + + @Override + public void onRepeatClicked() { + super.onRepeatClicked(); + switch (getCurrentRepeatMode()) { + case REPEAT_DISABLED: + // Drawable didn't work on low API :/ + //notRemoteView.setImageViewResource(R.id.notificationRepeat, R.drawable.ic_repeat_disabled_white); + // Set the icon to 30% opacity - 255 (max) * .3 + notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 77); + break; + case REPEAT_ONE: + notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, 255); + break; + case REPEAT_ALL: + // Waiting :) + break; } - } - - public void onOpenDetail(Context context, String videoUrl) { - if (DEBUG) Log.d(TAG, "onOpenDetail() called with: context = [" + context + "], videoUrl = [" + videoUrl + "]"); - Intent i = new Intent(context, VideoItemDetailActivity.class); - i.putExtra(NavStack.SERVICE_ID, 0); - i.putExtra(NavStack.URL, videoUrl); - i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(i); - //NavStack.getInstance().openDetailActivity(context, videoUrl, 0); - } - - public void onUpdateThumbnail(Intent intent) { - if (DEBUG) Log.d(TAG, "onUpdateThumbnail() called"); - if (!intent.getStringExtra(VIDEO_URL).equals(videoUrl)) return; - videoThumbnail = ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail; - if (videoThumbnail != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, videoThumbnail); updateNotification(-1); } - public void onVideoClose() { - if (DEBUG) Log.d(TAG, "onVideoClose() called"); + @Override + public void onUpdateThumbnail(Intent intent) { + super.onUpdateThumbnail(intent); + if (getVideoThumbnail() != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, getVideoThumbnail()); + updateNotification(-1); + } + + @Override + public void onDismiss(PopupMenu menu) { + super.onDismiss(menu); + if (isPlaying()) animateView(getControlsRoot(), false, 500, 0); + } + + @Override + public void onError() { + Toast.makeText(context, "Failed to play this video", Toast.LENGTH_SHORT).show(); stopSelf(); } - public void onVideoPlayPause() { - if (DEBUG) Log.d(TAG, "onVideoPlayPause() called"); - if (CURRENT_STATE == STATE_COMPLETED) { - changeState(STATE_LOADING); - emVideoView.restart(); - return; - } - if (emVideoView.isPlaying()) { - emVideoView.pause(); - progressPollRepeater.stop(); - internalListener.onRepeat(); - changeState(STATE_PAUSED); - } else { - emVideoView.start(); - progressPollRepeater.start(); - changeState(STATE_PLAYING); - } - } + /*////////////////////////////////////////////////////////////////////////// + // States + //////////////////////////////////////////////////////////////////////////*/ - public void onFastRewind() { - if (DEBUG) Log.d(TAG, "onFastRewind() called"); - seekBy(-FAST_FORWARD_REWIND_AMOUNT); - internalListener.onRepeat(); - changeState(STATE_PAUSED_SEEK); - - showAndAnimateControl(R.drawable.ic_action_av_fast_rewind, true); - } - - public void onFastForward() { - if (DEBUG) Log.d(TAG, "onFastForward() called"); - seekBy(FAST_FORWARD_REWIND_AMOUNT); - internalListener.onRepeat(); - changeState(STATE_PAUSED_SEEK); - - showAndAnimateControl(R.drawable.ic_action_av_fast_forward, true); + @Override + public void onLoading() { + super.onLoading(); + updateNotification(R.drawable.ic_play_arrow_white); } @Override - public void onSeekComplete() { - if (DEBUG) Log.d(TAG, "onSeekComplete() called"); - - if (!emVideoView.isPlaying()) emVideoView.start(); - changeState(STATE_PLAYING); - /*if (emVideoView.isPlaying()) changeState(STATE_PLAYING); - else changeState(STATE_PAUSED);*/ + public void onPlaying() { + super.onPlaying(); + updateNotification(R.drawable.ic_pause_white); } @Override - public void onCompletion() { - if (DEBUG) Log.d(TAG, "onCompletion() called"); - changeState(STATE_COMPLETED); - progressPollRepeater.stop(); + public void onBuffering() { + super.onBuffering(); + updateNotification(R.drawable.ic_play_arrow_white); } @Override - public boolean onError() { - if (DEBUG) Log.d(TAG, "onError() called"); - stopSelf(); - return true; - } - - /////////////////////////////////////////////////////////////////////////// - // SeekBar Listener - /////////////////////////////////////////////////////////////////////////// - - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - if (DEBUG) Log.d(TAG, "onProgressChanged() called with: seekBar = [" + seekBar + "], progress = [" + progress + "], fromUser = [" + fromUser + "]"); - viewHolder.getPlaybackCurrentTime().setText(TimeFormatUtil.formatMs(progress)); + public void onPaused() { + super.onPaused(); + updateNotification(R.drawable.ic_play_arrow_white); + showAndAnimateControl(R.drawable.ic_play_arrow_white, false); } @Override - public void onStartTrackingTouch(SeekBar seekBar) { - if (DEBUG) Log.d(TAG, "onStartTrackingTouch() called with: seekBar = [" + seekBar + "]"); - - changeState(STATE_PAUSED_SEEK); - if (emVideoView.isPlaying()) emVideoView.pause(); - animateView(viewHolder.getControlsRoot(), true, 300, 0); - viewHolder.getControlsRoot().setAlpha(1f); + public void onPausedSeek() { + super.onPausedSeek(); + updateNotification(R.drawable.ic_play_arrow_white); } @Override - public void onStopTrackingTouch(SeekBar seekBar) { - if (DEBUG) Log.d(TAG, "onProgressChanged() called with: seekBar = [" + seekBar + "], progress = [" + seekBar.getProgress() + "]"); - emVideoView.seekTo(seekBar.getProgress()); - + public void onCompleted() { + super.onCompleted(); + updateNotification(R.drawable.ic_replay_white); + showAndAnimateControl(R.drawable.ic_replay_white, false); } - /////////////////////////////////////////////////////////////////////////// - // Repeater Listener - /////////////////////////////////////////////////////////////////////////// - - /** - * Don't mistake this with anything related to the player itself, it's the {@link Repeater.RepeatListener#onRepeat} - * It's used for pool the progress of the video - */ - @Override - public void onRepeat() { - onUpdateProgress(emVideoView.getCurrentPosition(), emVideoView.getDuration(), emVideoView.getBufferPercentage()); - } } private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener { @@ -663,42 +434,33 @@ public class PopupVideoPlayer extends Service implements StateInterface { @Override public boolean onDoubleTap(MotionEvent e) { if (DEBUG) Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY()); - if (!emVideoView.isPlaying()) return false; - if (e.getX() > popupWidth / 2) internalListener.onFastForward(); - else internalListener.onFastRewind(); + if (!playerImpl.isPlaying()) return false; + if (e.getX() > popupWidth / 2) playerImpl.onFastForward(); + else playerImpl.onFastRewind(); return true; } @Override public boolean onSingleTapConfirmed(MotionEvent e) { if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); - if (emVideoView == null) return false; - internalListener.onVideoPlayPause(); + if (playerImpl.getPlayer() == null) return false; + playerImpl.onVideoPlayPause(); return true; } - @Override public boolean onDown(MotionEvent e) { if (DEBUG) Log.d(TAG, "onDown() called with: e = [" + e + "]"); initialPopupX = windowLayoutParams.x; initialPopupY = windowLayoutParams.y; - popupWidth = viewHolder.getRootView().getWidth(); - popupHeight = viewHolder.getRootView().getHeight(); + popupWidth = playerImpl.getRootView().getWidth(); + popupHeight = playerImpl.getRootView().getHeight(); return false; } - @Override - public void onShowPress(MotionEvent e) { - if (DEBUG) Log.d(TAG, "onShowPress() called with: e = [" + e + "]"); - /*viewHolder.getControlsRoot().animate().setListener(null).cancel(); - viewHolder.getControlsRoot().setAlpha(1f); - viewHolder.getControlsRoot().setVisibility(View.VISIBLE);*/ - animateView(viewHolder.getControlsRoot(), true, 200, 0); - } - @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + if (!isMoving || playerImpl.getControlsRoot().getAlpha() != 1f) playerImpl.animateView(playerImpl.getControlsRoot(), true, 30, 0); isMoving = true; float diffX = (int) (e2.getRawX() - e1.getRawX()), posX = (int) (initialPopupX + diffX); float diffY = (int) (e2.getRawY() - e1.getRawY()), posY = (int) (initialPopupY + diffY); @@ -712,20 +474,21 @@ public class PopupVideoPlayer extends Service implements StateInterface { windowLayoutParams.x = (int) posX; windowLayoutParams.y = (int) posY; - if (DEBUG) Log.d(TAG, "PopupVideoPlayer.onScroll = " + + //noinspection PointlessBooleanExpression + if (DEBUG && false) Log.d(TAG, "PopupVideoPlayer.onScroll = " + ", e1.getRaw = [" + e1.getRawX() + ", " + e1.getRawY() + "]" + ", e2.getRaw = [" + e2.getRawX() + ", " + e2.getRawY() + "]" + ", distanceXy = [" + distanceX + ", " + distanceY + "]" + ", posXy = [" + posX + ", " + posY + "]" + ", popupWh rootView.get wh = [" + popupWidth + " x " + popupHeight + "]"); - windowManager.updateViewLayout(viewHolder.getRootView(), windowLayoutParams); + windowManager.updateViewLayout(playerImpl.getRootView(), windowLayoutParams); return true; } private void onScrollEnd() { if (DEBUG) Log.d(TAG, "onScrollEnd() called"); - if (viewHolder.isControlsVisible() && CURRENT_STATE == STATE_PLAYING) { - animateView(viewHolder.getControlsRoot(), false, 300, DEFAULT_CONTROLS_HIDE_TIME); + if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == StateInterface.STATE_PLAYING) { + playerImpl.animateView(playerImpl.getControlsRoot(), false, 300, AbstractPlayer.DEFAULT_CONTROLS_HIDE_TIME); } } @@ -763,48 +526,56 @@ public class PopupVideoPlayer extends Service implements StateInterface { if (service == null) return; streamExtractor = service.getExtractorInstance(intent.getStringExtra(NavStack.URL)); StreamInfo info = StreamInfo.getVideoInfo(streamExtractor); - String defaultResolution = sharedPreferences.getString( + String defaultResolution = playerImpl.getSharedPreferences().getString( getResources().getString(R.string.default_resolution_key), getResources().getString(R.string.default_resolution_value)); - String chosen = "", secondary = "", fallback = ""; + VideoStream chosen = null, secondary = null, fallback = null; + playerImpl.setVideoStreamsList(info.video_streams instanceof ArrayList + ? (ArrayList) info.video_streams + : new ArrayList<>(info.video_streams)); + for (VideoStream item : info.video_streams) { if (DEBUG && printStreams) { - Log.d(TAG, "StreamExtractor: current Item" + Log.d(TAG, "FetcherRunnable.StreamExtractor: current Item" + ", item.resolution = " + item.resolution + ", item.format = " + item.format + ", item.url = " + item.url); } if (defaultResolution.equals(item.resolution)) { if (item.format == MediaFormat.MPEG_4.id) { - chosen = item.url; - if (DEBUG) - Log.d(TAG, "StreamExtractor: CHOSEN item" - + ", item.resolution = " + item.resolution - + ", item.format = " + item.format - + ", item.url = " + item.url); - } else if (item.format == 2) secondary = item.url; - else fallback = item.url; - + chosen = item; + if (DEBUG) Log.d(TAG, "FetcherRunnable.StreamExtractor: CHOSEN item, item.resolution = " + item.resolution + ", item.format = " + item.format + ", item.url = " + item.url); + } else if (item.format == 2) secondary = item; + else fallback = item; } } - if (!chosen.trim().isEmpty()) streamUri = Uri.parse(chosen); - else if (!secondary.trim().isEmpty()) streamUri = Uri.parse(secondary); - else if (!fallback.trim().isEmpty()) streamUri = Uri.parse(fallback); - else streamUri = Uri.parse(info.video_streams.get(0).url); - if (DEBUG && printStreams) Log.d(TAG, "StreamExtractor: chosen = " + chosen + int selectedIndexStream; + + if (chosen != null) selectedIndexStream = info.video_streams.indexOf(chosen); + else if (secondary != null) selectedIndexStream = info.video_streams.indexOf(secondary); + else if (fallback != null) selectedIndexStream = info.video_streams.indexOf(fallback); + else selectedIndexStream = 0; + + playerImpl.setSelectedIndexStream(selectedIndexStream); + + if (DEBUG && printStreams) Log.d(TAG, "FetcherRunnable.StreamExtractor: chosen = " + chosen + "\n, secondary = " + secondary + "\n, fallback = " + fallback + "\n, info.video_streams.get(0).url = " + info.video_streams.get(0).url); - videoUrl = info.webpage_url; - videoTitle = info.title; - channelName = info.uploader; + + playerImpl.setVideoUrl(info.webpage_url); + playerImpl.setVideoTitle(info.title); + playerImpl.setChannelName(info.uploader); + if (info.start_position > 0) playerImpl.setVideoStartPos(info.start_position * 1000); + else playerImpl.setVideoStartPos(-1); + mainHandler.post(new Runnable() { @Override public void run() { - playVideo(streamUri); + playerImpl.playVideo(playerImpl.getSelectedStreamUri(), true); } }); imageLoader.loadImage(info.thumbnail_url, displayImageOptions, new SimpleImageLoadingListener() { @@ -813,9 +584,10 @@ public class PopupVideoPlayer extends Service implements StateInterface { mainHandler.post(new Runnable() { @Override public void run() { - videoThumbnail = loadedImage; - if (videoThumbnail != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, videoThumbnail); + playerImpl.setVideoThumbnail(loadedImage); + if (loadedImage != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage); updateNotification(-1); + ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = loadedImage; } }); } @@ -841,4 +613,5 @@ public class PopupVideoPlayer extends Service implements StateInterface { } } } -} + +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/player/popup/StateInterface.java b/app/src/main/java/org/schabi/newpipe/player/StateInterface.java similarity index 71% rename from app/src/main/java/org/schabi/newpipe/player/popup/StateInterface.java rename to app/src/main/java/org/schabi/newpipe/player/StateInterface.java index 94ea41470..7b3681ab8 100644 --- a/app/src/main/java/org/schabi/newpipe/player/popup/StateInterface.java +++ b/app/src/main/java/org/schabi/newpipe/player/StateInterface.java @@ -1,8 +1,9 @@ -package org.schabi.newpipe.player.popup; +package org.schabi.newpipe.player; public interface StateInterface { int STATE_LOADING = 123; - int STATE_PLAYING = 125; + int STATE_PLAYING = 124; + int STATE_BUFFERING = 125; int STATE_PAUSED = 126; int STATE_PAUSED_SEEK = 127; int STATE_COMPLETED = 128; @@ -11,6 +12,7 @@ public interface StateInterface { void onLoading(); void onPlaying(); + void onBuffering(); void onPaused(); void onPausedSeek(); void onCompleted(); diff --git a/app/src/main/java/org/schabi/newpipe/player/popup/PopupViewHolder.java b/app/src/main/java/org/schabi/newpipe/player/popup/PopupViewHolder.java deleted file mode 100644 index 22895668e..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/popup/PopupViewHolder.java +++ /dev/null @@ -1,93 +0,0 @@ -package org.schabi.newpipe.player.popup; - -import android.graphics.Color; -import android.graphics.PorterDuff; -import android.os.Build; -import android.view.View; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.SeekBar; -import android.widget.TextView; - -import com.devbrackets.android.exomedia.ui.widget.EMVideoView; - -import org.schabi.newpipe.R; - -public class PopupViewHolder { - private View rootView; - private EMVideoView videoView; - private View loadingPanel; - private ImageView endScreen; - private ImageView controlAnimationView; - private LinearLayout controlsRoot; - private SeekBar playbackSeekBar; - private TextView playbackCurrentTime; - private TextView playbackEndTime; - - public PopupViewHolder(View rootView) { - if (rootView == null) return; - this.rootView = rootView; - this.videoView = (EMVideoView) rootView.findViewById(R.id.popupVideoView); - this.loadingPanel = rootView.findViewById(R.id.loadingPanel); - this.endScreen = (ImageView) rootView.findViewById(R.id.endScreen); - this.controlAnimationView = (ImageView) rootView.findViewById(R.id.controlAnimationView); - this.controlsRoot = (LinearLayout) rootView.findViewById(R.id.playbackControlRoot); - this.playbackSeekBar = (SeekBar) rootView.findViewById(R.id.playbackSeekBar); - this.playbackCurrentTime = (TextView) rootView.findViewById(R.id.playbackCurrentTime); - this.playbackEndTime = (TextView) rootView.findViewById(R.id.playbackEndTime); - doModifications(); - } - - private void doModifications() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); - playbackSeekBar.getProgressDrawable().setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY); - } - - public boolean isControlsVisible() { - return controlsRoot != null && controlsRoot.getVisibility() == View.VISIBLE; - } - - public boolean isVisible(View view) { - return view != null && view.getVisibility() == View.VISIBLE; - } - - /////////////////////////////////////////////////////////////////////////// - // GETTERS - /////////////////////////////////////////////////////////////////////////// - - public View getRootView() { - return rootView; - } - - public EMVideoView getVideoView() { - return videoView; - } - - public View getLoadingPanel() { - return loadingPanel; - } - - public ImageView getEndScreen() { - return endScreen; - } - - public ImageView getControlAnimationView() { - return controlAnimationView; - } - - public LinearLayout getControlsRoot() { - return controlsRoot; - } - - public SeekBar getPlaybackSeekBar() { - return playbackSeekBar; - } - - public TextView getPlaybackCurrentTime() { - return playbackCurrentTime; - } - - public TextView getPlaybackEndTime() { - return playbackEndTime; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/search_fragment/SearchInfoItemFragment.java b/app/src/main/java/org/schabi/newpipe/search_fragment/SearchInfoItemFragment.java index 6bf261e14..cb3869c01 100644 --- a/app/src/main/java/org/schabi/newpipe/search_fragment/SearchInfoItemFragment.java +++ b/app/src/main/java/org/schabi/newpipe/search_fragment/SearchInfoItemFragment.java @@ -19,17 +19,14 @@ import android.view.inputmethod.InputMethodManager; import android.widget.ProgressBar; import android.widget.Toast; -import org.schabi.newpipe.ChannelActivity; +import org.schabi.newpipe.R; import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.search.SearchEngine; import org.schabi.newpipe.extractor.search.SearchResult; import org.schabi.newpipe.info_list.InfoItemBuilder; -import org.schabi.newpipe.report.ErrorActivity; -import org.schabi.newpipe.R; -import org.schabi.newpipe.detail.VideoItemDetailActivity; -import org.schabi.newpipe.detail.VideoItemDetailFragment; import org.schabi.newpipe.info_list.InfoListAdapter; +import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.util.NavStack; import java.util.EnumSet; diff --git a/app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_close_white.png similarity index 100% rename from app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png rename to app/src/main/res/drawable-hdpi/ic_close_white.png diff --git a/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png deleted file mode 100644 index ceb1a1eebf2b2cc9a008f42010e144f4dab968de..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 221 zcmV<303!d1P)og+*{ z>6z1@lfD*AYSPav7BDt19=+BlEUV#Pe_cgeFA7o*e-4OJ%*1*LDWQwP&pUXO@geCxg CH6INC literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_pause_white.png similarity index 100% rename from app/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png rename to app/src/main/res/drawable-hdpi/ic_pause_white.png diff --git a/app/src/main/res/drawable-hdpi/ic_pause_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_pause_white_24dp.png deleted file mode 100644 index 4d2ea05c462291e4a4f8bd30856a25ad33fd420f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 105 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K;fv1aOh{y5d1PRu~43dBH9UPL5 z6iTt&V$nc&>_`Vn1*HHI9IZlT;SSEl^VRbdXz6 z;qoHig@_3?GBlOBFuZb6eK&Jo%4{mH#| XyMl6-y}oq;=qd(JS3j3^P62B5kK^|J))MUC zlz7ydwCsb8xn}W?^r?}5wtsjdmvX{3=JbT!J9sKPrYmk={m|7^>A|Z*7j^D$`>}ry Zqi^Z9GQN4ct^!@a;OXk;vd$@?2>?I&LQeny diff --git a/app/src/main/res/drawable-mdpi/ic_fullscreen_exit_white.png b/app/src/main/res/drawable-mdpi/ic_fullscreen_exit_white.png new file mode 100644 index 0000000000000000000000000000000000000000..364bad0b843bf6a17478979fb0e66915aa67d818 GIT binary patch literal 101 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZA`BpB)|k7xlYrjj7PU|_|fb^sd^~`LtLbIA@2_R9U%Rlu6{1-oD!MyHRzEdio=z*QTNc|^PhH$1G zLIT$m`Z~Df*BafCZDwc@{~PY978G? zlN%ZWKgf5mski^P@A&+mS=aI~*9HAMY}L0USsB(|s&9O`k8?Z75KmV>mvv4FO#pDq BA9(-( literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_fullscreen_white.png b/app/src/main/res/drawable-xhdpi/ic_fullscreen_white.png new file mode 100644 index 0000000000000000000000000000000000000000..c1dcfb29024fc0eec6fb8d2135e295b5205f1323 GIT binary patch literal 109 zcmeAS@N?(olHy`uVBq!ia0vp^2_Vb}Bp6OT_L>T$m`Z~Df*BafCZDwc^3*(C978G? zlNlNV5B&dc&wTj*fBB4ajKP1J`;5;VNZ`w^TDCoqfnmn3`o>w)@0Ebe@pScbS?83{ F1OOWIB9;IE literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_pause_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_pause_white.png similarity index 100% rename from app/src/main/res/drawable-xxxhdpi/ic_pause_white_24dp.png rename to app/src/main/res/drawable-xhdpi/ic_pause_white.png diff --git a/app/src/main/res/drawable-xhdpi/ic_play_arrow_white_48dp.png b/app/src/main/res/drawable-xhdpi/ic_play_arrow_white.png similarity index 100% rename from app/src/main/res/drawable-xhdpi/ic_play_arrow_white_48dp.png rename to app/src/main/res/drawable-xhdpi/ic_play_arrow_white.png diff --git a/app/src/main/res/drawable-xhdpi/ic_repeat_white.png b/app/src/main/res/drawable-xhdpi/ic_repeat_white.png new file mode 100644 index 0000000000000000000000000000000000000000..c13d0024287550e60ba5ef0ccb8e4a742b5f4049 GIT binary patch literal 258 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeG3?%1&o4*=J@dWsUxB}__Fko@OdIgZrToU9L z%+SDa@N3LeAa{|ci(^OybP0l+XkKQK49W literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_close_white.png b/app/src/main/res/drawable-xxhdpi/ic_close_white.png new file mode 100644 index 0000000000000000000000000000000000000000..4927bc242e23be272c9fd4be0f4da56a0e1c54d6 GIT binary patch literal 524 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=v;urWT!Hj|;y~N(Q~E&Fyd^u<8%L#D-{7p<|%4@n9yOn_`BhIN5Se>F5&yX=g3#NJr`MF8+b%+$B8BmnCIPndHJicwnxNiHsOkk&+*iSjJjmJe1$X)E`JjJT7)$8wm+rVMFtHWj=GUBk= z=ke_WBcq(Ni(Gt*T>KFvWMaekQTMd-+4Z${JvAkNz8{`iZ@=$N<$){y-?y+S-FBWF zc%ND6E)u%gY+Qf)aIpRD!%Emtu>HZuoi`qLflzYyjmJ==_n}6?N_~C6*PuJQ&feR6 zYOCY7g5y!UB2LPQOzopr0M`r!;Q#;t literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_fullscreen_exit_white.png b/app/src/main/res/drawable-xxhdpi/ic_fullscreen_exit_white.png new file mode 100644 index 0000000000000000000000000000000000000000..b7f4133fd978de01cb1e62b660d402ec92e3e4da GIT binary patch literal 123 zcmeAS@N?(olHy`uVBq!ia0vp^6F`^|NHCnYy)O!+m`Z~Df*BafCZDwc@=QEk978G? zlNYe`GzEV6&&bP0l+XkKgGeJs literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_fullscreen_white.png b/app/src/main/res/drawable-xxhdpi/ic_fullscreen_white.png new file mode 100644 index 0000000000000000000000000000000000000000..a0a1b4d4f3c5213f8803300e4428968bc037098f GIT binary patch literal 123 zcmeAS@N?(olHy`uVBq!ia0vp^6F`^|NHCnYy)O!+m`Z~Df*BafCZDwc@=QEk978G? zlNF?UngS2}`TzevKM&6zhgSzSh&FAUu~N}Q%S7jZ#sXI(Nl_`mr&>D`M0%JQOutw- TbRR8?0GaCP>gTe~DWM4f)Gs9S literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_pause_white.png b/app/src/main/res/drawable-xxhdpi/ic_pause_white.png new file mode 100644 index 0000000000000000000000000000000000000000..3ea7e03e5dc315fcca0178a146ad2eb1b013c15a GIT binary patch literal 110 zcmeAS@N?(olHy`uVBq!ia0vp^6F`^|NHCnYy)O!+m`Z~Df*BafCZDwc^3*+D978G? wlNIE9ngS2}|Nozzqcc7u1OYp847l7F8Cs>jNuFVdQ&MBb@0ISd)(EtDd literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_play_arrow_white_48dp.png b/app/src/main/res/drawable-xxhdpi/ic_play_arrow_white.png similarity index 100% rename from app/src/main/res/drawable-xxhdpi/ic_play_arrow_white_48dp.png rename to app/src/main/res/drawable-xxhdpi/ic_play_arrow_white.png diff --git a/app/src/main/res/drawable-xxhdpi/ic_repeat_white.png b/app/src/main/res/drawable-xxhdpi/ic_repeat_white.png new file mode 100644 index 0000000000000000000000000000000000000000..bf76079662fab880c2fa94aa0b0d65eff7de2e49 GIT binary patch literal 356 zcmeAS@N?(olHy`uVBq!ia0vp^6F``W8A#63;L-+CJOMr-u0Z-f3|JhnUIFAYmjw9* zGc+(9{2DWrfq{|5)5S5Qg7NJ&!z^Y;9=F7B%Qbxu+`bnq)^lQd{QHH17EkjfRxz^= z9MQaWU&DF+9DVUE^YFe;+oS(ooxEVWKnRPeWA_5Vrmh8Q8GLs&Q+v>CISx)c~QR2_T+1XUQ;vaI0L zYGPTC&h*7MAg_@xx54g(!B4&yju&JXR4_kbk#yt=;BoMqAh?9#C<7{YPMHf#eELEK zjj0OCCpfv9g4k=i+$IQ%uq+iyVX<^{4A62!;ojq{%Bg6pxxoMXV3^oR`w$6W@Gy9~ L`njxgN@xNAb-;WW literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_close_white.png b/app/src/main/res/drawable-xxxhdpi/ic_close_white.png new file mode 100644 index 0000000000000000000000000000000000000000..1ab2312754a15f2f679d1f0361cab0aa9954ce81 GIT binary patch literal 707 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE8Azrw%`pX1#sNMdu0Z-fiD3KGM@>L&0wqCy z!3?}ShmRdO`suT!jkVQ|?c1(&>uh9TVAA(=aSW-5dwb`!pLC!ATVf7V-(l6d|I2$f zz2Z#?z8oAmWoi9?(MLwjedmJbUyI6KyA_4_e{cQy*L)8?71|&D|M_E0?8pBVcit`k zAs<|@=KseXd*?r#-_-m!c*83=#rpq#)Arc^P6vN1PdK|jz~GetYnr z-~)dbF#K5F@cTQrUHjiy#_#9rR^AkKrj z?G3;0GQBJ3-f>@h$DAk6cQ-IS5a(*RueG3Vf5Wf8Os~Fkuec9Xw!h)`U#558xp&m> zfAH1&((6{Z9Ew5&XTg2_34i~@+aAytao8W<@VAcf`)}4e@5KxD#~(oE82n@UCHBbv z$X}_q{F3!kUL>wNp82qKS-98prT4zu+b?FiU!S_XxW?W#{>ksi#`o#0`{s-Os0(|5 z#0h%v`(|bEeSiPwXc8!@A`*W8yeW43Ja-9N7$616{`e0c*VZq6@c2W+_uDhWC*OJJ zf1CC3bJhd=T*uCHhyC5W{q{Fj&&T(*ZtPu@^(UKk_4JnB`i%)6XKnkqUhGZm6!U*~ ro9?dWdLJwH(?0(CO^n!HdtUzRDW+vbYyai|lNE!ftDnm{r-UW|G*Ff) literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_fullscreen_exit_white.png b/app/src/main/res/drawable-xxxhdpi/ic_fullscreen_exit_white.png new file mode 100644 index 0000000000000000000000000000000000000000..b47b3f8bdbfbd9b67d208988c6856b0bdc3faefa GIT binary patch literal 125 zcmeAS@N?(olHy`uVBq!ia0vp^2SAt+NHA0_4_pPLm`Z~Df*BafCZDwc^2|J4978G? z-(EH3Vo=~{yZEmDrTr|Ks8*)?(k?&BVv89(&rVa|u*|9yYmxqNcdyW|_=yTVj5%4K V{s}ble`^Gp>*?y}vd$@?2>?OkCyoFB literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_fullscreen_white.png b/app/src/main/res/drawable-xxxhdpi/ic_fullscreen_white.png new file mode 100644 index 0000000000000000000000000000000000000000..ea9f18ae63d133b7b3214e6f6e9addc7a1696884 GIT binary patch literal 124 zcmeAS@N?(olHy`uVBq!ia0vp^2SAt+NHA0_4_pPLm`Z~Df*BafCZDwc@=QHl978G? z-(EB1Vo=~=as2;3^QhawTN56ot!iG&EUDPAbM8g?mfiKNjtp{7%{4gU=KPz$7?Jhq VpTN51)2@Sz^>p=fS?83{1OP~PC}02p literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_pause_white.png b/app/src/main/res/drawable-xxxhdpi/ic_pause_white.png new file mode 100644 index 0000000000000000000000000000000000000000..76482b1fdef47a217988076172dd3b1df1b694a4 GIT binary patch literal 111 zcmeAS@N?(olHy`uVBq!ia0vp^2SAt+NHA0_4_pPLm`Z~Df*BafCZDwc@-#eM978G? z-=5qERKs$_;kSP8IllLomhAX7Y015J3=KLUoefJKu<6RztTS}WSqI{Ky85}Sb4q9e E08jfNfB*mh literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_white_48dp.png b/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_white.png similarity index 100% rename from app/src/main/res/drawable-xxxhdpi/ic_play_arrow_white_48dp.png rename to app/src/main/res/drawable-xxxhdpi/ic_play_arrow_white.png diff --git a/app/src/main/res/drawable-xxxhdpi/ic_repeat_white.png b/app/src/main/res/drawable-xxxhdpi/ic_repeat_white.png new file mode 100644 index 0000000000000000000000000000000000000000..a59db47ee78238474efc9bd8b5d261c813308909 GIT binary patch literal 403 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvK8A$4HYMBD0cmjMvT!Hj|7_c~Cy#mN*E(!7r zW@unI_%&uK0|TS6r;B4q1>@W63wvE1IUFv=>-z?9O>*!QX;OZ3N>o3;_-Kvkb$Q1$ zfttMS(w?94=l|KhXo02#SD1zd*Q}<639U`Ixs?pRX9Wa^n6Nq@}Z)+O@h?T4-%zvW{C3{3`4S3j3^P6 diff --git a/app/src/main/res/drawable/player_top_controls_bg.xml b/app/src/main/res/drawable/player_top_controls_bg.xml new file mode 100644 index 000000000..b7cdecc87 --- /dev/null +++ b/app/src/main/res/drawable/player_top_controls_bg.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_exo_player.xml b/app/src/main/res/layout/activity_exo_player.xml index 5653532ec..72f8aa897 100644 --- a/app/src/main/res/layout/activity_exo_player.xml +++ b/app/src/main/res/layout/activity_exo_player.xml @@ -1,15 +1,303 @@ - - + + android:background="@android:color/black" + android:gravity="center"> - + android:layout_height="match_parent" + android:layout_gravity="center"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/exomedia_custom_controls.xml b/app/src/main/res/layout/exomedia_custom_controls.xml deleted file mode 100644 index dedaf7908..000000000 --- a/app/src/main/res/layout/exomedia_custom_controls.xml +++ /dev/null @@ -1,180 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/player_notification.xml b/app/src/main/res/layout/player_notification.xml index 43ac993ca..22a60418b 100644 --- a/app/src/main/res/layout/player_notification.xml +++ b/app/src/main/res/layout/player_notification.xml @@ -65,7 +65,7 @@ android:background="#00ffffff" android:clickable="true" android:scaleType="fitXY" - android:src="@drawable/ic_pause_white_24dp" /> + android:src="@drawable/ic_pause_white" /> + android:src="@drawable/ic_close_white" /> diff --git a/app/src/main/res/layout/player_notification_expanded.xml b/app/src/main/res/layout/player_notification_expanded.xml index 0fb6a8eb2..4a81d2ca3 100644 --- a/app/src/main/res/layout/player_notification_expanded.xml +++ b/app/src/main/res/layout/player_notification_expanded.xml @@ -58,7 +58,7 @@ android:background="#00ffffff" android:clickable="true" android:scaleType="fitXY" - android:src="@drawable/ic_close_white_24dp" /> + android:src="@drawable/ic_close_white" /> diff --git a/app/src/main/res/layout/player_popup.xml b/app/src/main/res/layout/player_popup.xml index 6d1860408..a3b2b80b6 100644 --- a/app/src/main/res/layout/player_popup.xml +++ b/app/src/main/res/layout/player_popup.xml @@ -4,18 +4,29 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:background="@android:color/black" android:gravity="center"> - + android:layout_gravity="center"> - + + + + + + tools:ignore="ContentDescription" + tools:visibility="visible"/> + + + + + + + + + + + + + + + + + + + + + + + + android:orientation="horizontal" + android:weightSum="5"> + + tools:ignore="ContentDescription" + tools:visibility="visible"/> - - - - - - - - - - + tools:ignore="RtlHardcoded" + tools:text="1:06:29" + tools:visibility="visible"/> + tools:text="a long, long, long, long, long title"/> + tools:text="a long, long artist"/> + + + android:padding="5dp" + android:scaleType="fitCenter" + android:src="@drawable/ic_close_white" + tools:ignore="ContentDescription,RtlHardcoded"/> \ 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 263c6bc14..f91c51f3f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,6 +25,7 @@ Settings Use external video player Use external audio player + NewPipe Popup mode Video download path Path to store downloaded videos in. @@ -75,6 +76,7 @@ Other %1$s - NewPipe Playing in background + Playing in popup mode https://www.c3s.cc/ Play Content diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 606da4eb0..9cf8160c3 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -2,7 +2,7 @@ From 71ae342f520e155c097798b4edb5d738cf399331 Mon Sep 17 00:00:00 2001 From: Mauricio Colli Date: Mon, 27 Mar 2017 10:12:22 -0300 Subject: [PATCH 3/7] Implement screen orientation toggle --- .../schabi/newpipe/player/AbstractPlayer.java | 6 ++--- .../newpipe/player/ExoPlayerActivity.java | 17 +++++++++++++- .../ic_screen_rotation_white.png | Bin 0 -> 976 bytes .../ic_screen_rotation_white_24dp.png | Bin 858 -> 0 bytes .../ic_screen_rotation_white.png | Bin 0 -> 673 bytes .../ic_screen_rotation_white_24dp.png | Bin 639 -> 0 bytes .../ic_screen_rotation_white.png | Bin 0 -> 1256 bytes .../ic_screen_rotation_white_24dp.png | Bin 1191 -> 0 bytes .../ic_screen_rotation_white.png | Bin 0 -> 1897 bytes .../ic_screen_rotation_white_24dp.png | Bin 1873 -> 0 bytes .../ic_screen_rotation_white.png | Bin 0 -> 2553 bytes .../ic_screen_rotation_white_24dp.png | Bin 2611 -> 0 bytes .../main/res/layout/activity_exo_player.xml | 22 ++++++++++++++---- 13 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 app/src/main/res/drawable-hdpi/ic_screen_rotation_white.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_screen_rotation_white_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_screen_rotation_white.png delete mode 100644 app/src/main/res/drawable-mdpi/ic_screen_rotation_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_screen_rotation_white.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_screen_rotation_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_screen_rotation_white.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_screen_rotation_white_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_screen_rotation_white.png delete mode 100644 app/src/main/res/drawable-xxxhdpi/ic_screen_rotation_white_24dp.png diff --git a/app/src/main/java/org/schabi/newpipe/player/AbstractPlayer.java b/app/src/main/java/org/schabi/newpipe/player/AbstractPlayer.java index a9987bee9..1c6d4822b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/AbstractPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/AbstractPlayer.java @@ -18,7 +18,6 @@ import android.preference.PreferenceManager; import android.support.v4.content.ContextCompat; import android.text.TextUtils; import android.util.Log; -import android.view.Gravity; import android.view.Menu; import android.view.MenuItem; import android.view.SurfaceView; @@ -232,8 +231,7 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); this.playbackSeekBar.getProgressDrawable().setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) this.qualityPopupMenu = new PopupMenu(context, qualityTextView, Gravity.CENTER | Gravity.BOTTOM); - else this.qualityPopupMenu = new PopupMenu(context, qualityTextView); + this.qualityPopupMenu = new PopupMenu(context, qualityTextView); ((ProgressBar) this.loadingPanel.findViewById(R.id.progressBarLoadingPanel)).getIndeterminateDrawable().setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY); @@ -419,7 +417,7 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa if (DEBUG) Log.d(TAG, "onBuffering() called"); loadingPanel.setBackgroundColor(Color.TRANSPARENT); animateView(loadingPanel, true, 500, 0); - animateView(controlsRoot, false, 0, 0); + animateView(controlsRoot, false, 0, 0, true); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java index 58e93fc61..3a7c5e085 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java @@ -5,6 +5,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.ActivityInfo; import android.graphics.Color; import android.media.AudioManager; import android.net.Uri; @@ -173,6 +174,12 @@ public class ExoPlayerActivity extends Activity { getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); } + private void toggleOrientation() { + setRequestedOrientation(getResources().getDisplayMetrics().heightPixels > getResources().getDisplayMetrics().widthPixels + ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + : ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); + } + /////////////////////////////////////////////////////////////////////////// @SuppressWarnings({"unused", "WeakerAccess"}) @@ -183,6 +190,7 @@ public class ExoPlayerActivity extends Activity { private TextView brightnessTextView; private ImageButton repeatButton; + private ImageButton screenRotationButton; private ImageButton playPauseButton; AbstractPlayerImpl() { @@ -198,6 +206,7 @@ public class ExoPlayerActivity extends Activity { this.brightnessTextView = (TextView) rootView.findViewById(R.id.brightnessTextView); this.repeatButton = (ImageButton) rootView.findViewById(R.id.repeatButton); + this.screenRotationButton = (ImageButton) rootView.findViewById(R.id.screenRotationButton); this.playPauseButton = (ImageButton) rootView.findViewById(R.id.playPauseButton); // Due to a bug on lower API, lets set the alpha instead of using a drawable @@ -219,6 +228,7 @@ public class ExoPlayerActivity extends Activity { repeatButton.setOnClickListener(this); playPauseButton.setOnClickListener(this); + screenRotationButton.setOnClickListener(this); } @Override @@ -285,6 +295,7 @@ public class ExoPlayerActivity extends Activity { super.onClick(v); if (v.getId() == repeatButton.getId()) onRepeatClicked(); else if (v.getId() == playPauseButton.getId()) onVideoPlayPause(); + else if (v.getId() == screenRotationButton.getId()) onScreenRotationClicked(); if (getCurrentState() != STATE_COMPLETED) { animateView(playerImpl.getControlsRoot(), true, 300, 0, new Runnable() { @@ -298,6 +309,11 @@ public class ExoPlayerActivity extends Activity { } } + private void onScreenRotationClicked() { + if (DEBUG) Log.d(TAG, "onScreenRotationClicked() called"); + toggleOrientation(); + } + @Override public void onVideoPlayPause() { super.onVideoPlayPause(); @@ -348,7 +364,6 @@ public class ExoPlayerActivity extends Activity { @Override public void onLoading() { super.onLoading(); - hideSystemUi(); playPauseButton.setImageResource(R.drawable.ic_pause_white); } diff --git a/app/src/main/res/drawable-hdpi/ic_screen_rotation_white.png b/app/src/main/res/drawable-hdpi/ic_screen_rotation_white.png new file mode 100644 index 0000000000000000000000000000000000000000..b81f222469787abac93f5b3731e9e9330d337039 GIT binary patch literal 976 zcmV;>126oEP)#O+>seA~{r=l!UwF~V&VZVv(l?&fnrTpv#d;d21E%U%Z(2B0phXTC!=LtB z6ee-OPQCR(OAILuTN)=}#KUz#OPn;hQw`D{)77?5s|u*WiIYBeMTtSHqo6qFV3@Q^ zvckvON~0c$fa09(lI_!y_5xm$Pu3Y$8uxSr6z8N6=~YQtOTW^D=K`Q8Cq&vHHTI;7 z3hAi;D8dPlrrsK7<;Ja#fa09%BdzkI+y!^WK~r!lkXHLy?zp}HD9&k+MuLC1i*oNZ zL2*t!(&KUy)+wMkr#fl3+>p5i6z9|?^*Pymk057;NDpcQ&bhIG;+*=VkK}e1P@Ger zwA^LD8S@G#&Z$q@E4N9K>T{Z;Wzsae!vx^iWY9uGAx?eL5x`ZqDj@rVocg3qa$5_i z$5@b4pLBzZfWrlJv)n;Rnq|_bfKfe>PiW>zJLK*zhX?2wX|3Fra=6HF$4Gs~0sl|R zmKy08>3hJTq&rB*0N*B^Af0ZK@{%qiC0#~Jx{Q=`87b*9QgR${q&rCEi)~2rh0A82 zRDK=(ZEnX%BNi9XR=Inmj*&hnpfz&aq>hpPFeiiN8Uq}XW}S3cZb1empUKL;Sth+A zcU~%>?Q$EX`lJAdeg)(yx6K0!ntteJIr@ML7jD-*12kYwMGuoB~Q-lKW0?Wm16C1kH0w?p>)W zDZpuhl83bAHdZHvIMP(lxyIaEmDJ>fKrJWa&RA8A)Z_#~$!eo=KZHojo1`!&07^C( zmpkjeYbV7yF;Mc1w%nK}q{beLk^-CnD0#k+FrX!sc?)|&q!=dvN;Vr;8ZOUk{0~B; zI41x~9yO}e_KB5}cOXPM9OuM9$z2AOxZ+Eb8 y!%>$Sq}`nWmCbX%7YsOJ&`&P8>WVjNfc^#TcLVM>+zAT+0000lR!QAms~3=4d3Iv*F$x%POyb7!Vgy_?CL^E>DF zooDWSo^!Ws8REhMp5ZGj;W}Q!&SCnt8NPs*u{Ficq2r&|uK{ylGtE!p_b~VrBPqAg z03q~mW5=-OvsjC!Pvi48b|=kzg1CzhQ_Ve0Y}EWzYIF@VcoN?~t3k|R8GGXU@wh+W zCmbHs{B$zEn&LBf+y&yfT8~kD658)EIf!|+q*og9(+Oe;qbas-W2{B{ zZs^6yuHZ6SuQad1e8L-;28eOZUVvB<6D;9h0R{4XF@G6Y8O5fw) z?hps#$H0B;>!;}dg}`DgM?PN1W7)(q9@<2Rd)S+kcO!AER+Lv_vmutTtxDX8;Cw^T zczQY>+312sSQr1H`~LCN5at z>gVdo#c2LcOM@}xpcxnf7rUB=;3fPSt8k>H(Wr9dQjXxMKFlvA^RsQjH>2?by$6d7 zG2NoQ9J&t9^djA*j7#t_w)cQo3hm=22wX|#r(4Za(WEjs!mavBNCPK!D+6f-F_Usv zBfi$Rc`81vjL?*k`gp|mD0{+O(q@P!%3N_RnV%ZSJT;!Melo5pFB#R2*^xG_fVdsG zr>SOkL*}XR4!nim{QK%)A!$dH*EP!P*vX;HYasja9)4+nIG1!=aRkqAnt2UmCtg*i k+7fj()`=8jQXj;@k zG^Wzr>R>U!zd*6Mh1kN$$Ki6ggAaK*$+_=ue!rjni6+m}?s?*GTfTO~+nyJtg=Xuz z4*=F(b^36?5f@xCn-k!X8=gBTaMBv6ER6{R002Wh$C?8teFuP*eF^TG%YkzK!9GOa zs|J7_uN8naf7JC z`Gn}@gemLbq1hZr@(qb;+u$di9LV$I9WN5+?1H5X$nwpJMevWV5aoPxqT@dJyar17 z=EOXB=qXXkmx)!8Y!iG~14pbC@=b}$;M*FwP|WAVZF0hCPf+QG5H5iSPKefs@4@RK zgjp51(<1RP_$-XJM9ah*;Hyxt*8*`()bk?vHB5-87uq7$aw1Ha2oom4?_h3HM7Ra! z#dua*AeL+33(yxW6P2?g;7xE_v`B1$iz8sh9v(Z@8qo)w9|7S9@Q!GO=mTzw8n^;} z5iJnc0N*%122L4*SMo18QO=Kna2;&wG$so98hF+YSQHJ3LcRvV3fOh-fW&H^9|K2i zfNj%dqL5F4@PZxiv#Bys$frP9P=O88Iia5ry)2&t;eC7HPcMslK5`}_(tH63AE>~; zmK_zsC)mu1G+zM1f+65H3qFOMu$AXaK$x=u0DwV40YHSX;;O6=Xyaq=92Hq6p3OE3Ub0qJ8OW+(>NP)Km zSOsQ)vq&-vtn2p(u&4Z`ZtyCwspOSlydg;&7|D6=t|s*xSS*8Y#qc0rm*i`Kqzdc; zKYXQea9i-@k_ziUN0M6|1Pp0C-W0*h-=WK&mgKNN@(lQOT_x~b@W zWr<#L_TMD9&Fe*QUsJg!z~nzjYJPAB_!uR6$tf_EC>dA7w9L%znAyIWHGqDfRH;-B z&FsFJU6|QUqU3>@-7~YcH)iVDr#Z99)aM;wx`){>>3iA+s$t-*KJTFdN!RWImUH0> z4&yec-CIGs-i`~q3s@0+EAM%qw20eknP) z6p5em5X{d;NZld^2s_K2X@G9NiZGQ^g!OQ95+InLCak$+!W2#uwwH!9 zKsR4SXyF`TThajKd=+6KCBkN>0m}I*!c&CxFeVL9&Q}q36826Spq;NFjOGYo_rw7G zd_7?rCA{ybivjxidcv!OEsX*C`Fg@A{=z%VRWU$6Ur$&;*s>U)pRXqrIDmH`1<=pe z5LyYF8Uyt6^@IZ5c-tKyou7fQ7Q4t>g*U+HO28!jd?{fhr|?#k0~$D_pD!kC!~3)x z(59a+C(OVbU}Ox~p`R}&)YFSMCkE`n+o+!`BD{z9QViIIcbo~sl<)}NH!)xXOz(1Uk}5nemwfba?4!$y2-azJ<#xu`uPs1{0&~g)CB$f7*Njvyb>*Hf_{DsFs2iBgvOc^)+XjB0DkWi zwiCNdLJt$w^V5JE=_BlE1wlJM4KN-cti&P(K|4PUFy18W94!ihc77UgE#DG$ntQW9 z!knO-p9UDs^b&T8X;mbwNzcy$Fz%wCumM^X1m*lRz?esouo9h#C*Ext2#NU$fU%4r z;y%2n6CgBODPg}jG2}50WuI0^Hl(29{t3Xc!M$J^D__<^OXQ&GJTYH zf)46^eg?wk#C$QpXl6f^{mc@c1IR*1&DR5r5o{%0W5!yL}w~uD5wg97^M>&?AKojffVmI5EO987Tz^{Q0e#uObo{{zHxSx+G z>Ir&4>Y8|hFBrOV1VeFSX=N)vFjRYjp}bK{VI?oJgT4I9N%}cYkzOWO5AYvvcZv9C SVTN7+0000;*9}C?!)Sb)_t?1|A+zTt~?sLxG zx#!&5H9YI}{r2m;3X+onb46r8MAnGNGZA?zA`eAmQba~YWT;RmJkG0sCI&13?htR| zKo_vM)}WsN+kq7hUz!Mje}SJ|`FVrS1%6c^Q$V{Tt`(R_5drWV=mF+b3;t#dNLK|S z0R99%&KrJMHGBg+FcAAzPUaHe3b`$3ov5fzXO_qK?Bk4$e$0~F8WWZ z>-9i4@Y2-YpEdj~(_pXiHvu<{h%$hph_;v>>m0LLxrieCXdJwXYXk-iz@flit)>^i z*OkD}bHH~d#5V!g4ZwlGo-M$T%AW$(Wd*;-Bd-y-W<(qg?A;1Xs{DIELn!>1fiFAf zQ$*P1bTClF2f*Jd|8xlaZ@$YWp-uyE)rdF}SffMbz69P)OVCGM34V7de2NI~jSbY9 z!914~Y;QJih&l3RAmWM<;k_q-PgUN3z;a*sU6sP8h&URMukS&<)>+5&JLzj+XIAh= z#APF*$1``k%DHcyTMYb;tl?F!Ilv_&;+RKX1JM&}$v)K1PUV;Y>hltyL|il?dOh;` zReZa4tS*jx$@+jqtiMJgP9(%{(ksOkaKaS30L;!=#4O-v z)7N>&1(vFqCvgQ`B65ArixKfdB?XVyNJM|0A}-cQ#E+E}v>UrxBKo{U08drWlK;7g zGp@k9D&X^kg0X z0K14@s!BEvw7LRU0IyWw#(*Y=^AG{FdBLj$eHx9HO;d8SLq^2aN<;v=4g76jep=Ey zzzdc1ZD7-GBVrI}o&mgF^LKy+zR71);$Oh~fz1yZ5d%PDKz_R!`7vN#2tXsz=ev?U z0u}{|*k?ox`OF068+L-EHJ?yuJ<*3$4bT!;BRWn88eR znHo1;GeP-jb1QVf;sEv=fJtC&Ol%vDQ^HZ>hMoyvZ!Yi#KsR_%9NBsmbF%1j9(BZ= zPDH<@bnTH$;T6D_ME`NM-6Q%jcgkFM-fSq}*bfZ+Fwvj=d<<9x{Ena91r|DLd><1( zk}JFdm`(KT^E?JboAU@HI_LEX_e)ja4a9QbtcMWi9kiIg4cJgsa0keu;<6YE6_KSC zvM2&96OqqEWTS|@Cn76FWU+`ei^v}$vbj(wJjI@ diff --git a/app/src/main/res/drawable-xxhdpi/ic_screen_rotation_white.png b/app/src/main/res/drawable-xxhdpi/ic_screen_rotation_white.png new file mode 100644 index 0000000000000000000000000000000000000000..a160572a4efdb1d436bde5350b2f4a7e68ce81d7 GIT binary patch literal 1897 zcmY*adpy(YAOCL4<euA9xs@EF z8F>lm(CJtq$*jn1$R#tZTvilTA&xW7U%x+|=lOiz&-?xPyr1XwdR|YOkGCsaZHF2F z0B|=l=%-j28$)%AV)mj~+5>=!jT?B7ngE#{Ii8Uhq1!QEkE>^N#YIQApLMg^PI4pn zet~A}INcMa8&s~(-_{_u`i1qe+r}}N# z^!HUpW_6g!xsHE`pAC&>5Nz$m)3IwR*jJbgT_+ zOvt{$U;MlA>RoXA2jzMgK%!L{Z?jSaGIRibdO z8gE6o`G+?n$bySeXd!j*Ah)ec2Y5jXqEIJPI7z9CTSymJuaWx4$dA%9cz(*MFJE-@T>|AW&@(d23BR!EN2BKUM3~7DnZ>ZIkG(v#uanG zm)N%CZRZbg#=avp*evfzns$fH&?b;#x2iWH1$PPjY3sx{r>VQCa{PSSo{88?nh^>D z8(H(*U)nUJU`&H8&%f4VjR}-XZ2gQ~bIo`ZS%55#RBtw~SBQjZiTd6nc&GnB-Dacz z)f!)G=V<>sq>G^=bL(#2eGj|Pq7Hamj+EoK!`iQlZDr#xJ;pT8nI#+OQ z$VhY;GdwdFPy!tWl+3sT;!mJjWwwiw*-9R6Jw%NkaHxp^ZZmhT=$z?4R*1z2PsXnh z0`I}DVG@Unn_lnLEu6N!EK0BfJKtnQzWSwD@}XNC5kp@vgc450&@XgMBOIZ9CUytF zOdyt?q2X&vGC;ey$P4T9`TxIVKEgz)d%aCDe7UUYvXHhL$~^Bn+OqHgp4L)!4(BIwGsV^2_4yML?(X0{1S|G3Z$>4pRHz+4=qH>L_aTLeu z0F$MH%YEOH;aEJa{9-NoC_a!RUj0t8MLkrZ_%kJgmNAa1AlfMkq4?&2#4%MDfxGV|hB-J^BQHBJ@yq8F9jtMIA;!Fx*`GW>2 zWL}wyUH8;E&Nr=cJ;ZNw*2Ep;h3V9cz$Ztz2Q6!iMH`9I>dhEIaclfxGYIplt7aHB zBroRvJhPWO{{D2CD+x0$gy|319O3SW7sk39l1Lb-0!Y_4yC<=iQt=^d$Sx(kpRv?3 zE4N?u-eQ6S7_?!;WO$MJ*I3w@Pd>FNL4SNFR2=S%=^19tVfl(F#^R?ew^3qPZF!CI_Y{)+I*pg%D ztb16)sg)&+WW4>0sa+7R_wRz5)dGJPEhS*#qM^|VrQ`3CPDg0&{gocN^X`#i%k_{S dbV}DDF4vE~o&CV0D85j@?XWjkd+5ZKe*x-?e6j!l literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_screen_rotation_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_screen_rotation_white_24dp.png deleted file mode 100644 index cc0fa87f734d97a79834304f0ce6fe2b9c4ff88e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1873 zcmV-X2d?;uP)yHO;!4MIKWnc!zfuIsZLp0uv2i%B& zEF$}+C`%v+F%pfk%65<_#)}uXaPWY7Ga5}ixGM_Gz#vP;i7XYAQE#xI%7VP1XTd*4)dS9ibalBo~?FBxN28)GoWoHfRrHpZMc#xxjX8sp#J8e_gM zl}fE$m1ma$C;{7X<8R;-U@1`D)p-|+*raVKOln6f@HsFC=+V{r=LXO#MNA3Un!%)Y zv;b>>K1I|L3_x#SCvZA07)YCC;i-$?XIxB(hMb+pHpc>e(6!H^Lupci2?XMP;BJcmge5@Xg2G4}yK zDjKc8lFq8%ir5_KuxbOIPQ)%%3fKn>aOk`e$8NC1^dP!YYO`rosJc@Dr~#Uk|JYn* z%rYhUG$-x{Qsy-dIH~9?^{F*8upd?n9b=a$|K)waz(Dzp0FEm8O$wo56CX^p-pFiD z8OH*}r2PR+szpBh&jVvZXh@<%fJ1_4D=;e~*Wv@d(fghshojb-L6@Z_9t znMpzJ+JWJ~DM91i5d7^THgo3Z%b7+{fN zQbWL*v{K-20dCGiT8n-upB^IdGOf9aNrytrB%mAcmB7Ewmryh3a%gkPH0Q?VOC}8o zz&{eWnBe{gsL4uH4;)d=})XMtPN4DNDs%5s^Fe7OBi&ch9OS>Rpi8Z-v`j9-o8XD z>T)#L1`M*8GsQQN5I{e1H<9D&2D~9~-mL}iR?L~~n?Ohqz2ecXZnd7A_X~U-FZn

>U7s3pO*M+zJ*h;9#3RbT+K6qCG8+`!ujo;H#a(tN>`;9HwX3~?vDhmjUNzK5B6ytX$#+`Hx zu&t;o)ubo#nB*fk4E>(0Qcap@G3oZs5UiLR*R-&h^vC}u#i%rso(e7)0KkN1J<*#f1Pgygofp7(#iSOiNx*lB0bisPEbJ{uD8wcuAJn&-bf-2b+0=AA zgP`p_S*4n!9d6A?PbPtNLU7*{F*GbQ>0>GQ5g{;BcPKEF=n;ct(#&9$1%zT! zD{yNvun!%d0lnoRIK*O7rf!d-fV}l#loa?|BB&YY<{@ZniJ2Xwsz5-#vAvA+PK=Jt zkpTC43fEdpnv=V-001TM>6TToZQ;$=rHD#3B4zXoF>p?N{2~{JvoapCl>3C{Ae}g z_JDygUv87oyFG$*#cHCXiJBTEQU4R&)f!bq+cG6QpOVAY3X`eJ!>549k$VH1fsuhU zyiMv-bIOv?b_kvzJc8dbnA zipHu$Y|u8mARyWyC?y5(1Hv;TV0|J^-D?`hYgadb<#CL*bt*^)el8F_!#<-F&<4Cu zeC*@c%5BlGnY627&1nZP3j0?TE)Z?@w_%e*!*=3Mq7fKWRQ;&{T#tS0bsB7OX!Hes zc5pNiXtQ0X02oX9&rnF%?BK5=`VfkQeOwD4SpvEPD}cW|O>)hoooO!`j&7(lcyem7UXLS@7D zN;0X?M1bpzF{6wz_ZVa9w4 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_screen_rotation_white.png b/app/src/main/res/drawable-xxxhdpi/ic_screen_rotation_white.png new file mode 100644 index 0000000000000000000000000000000000000000..3cde2bfef0b48b1884f2676bc59071875531d3e7 GIT binary patch literal 2553 zcmZuzX;_oT7M>)ZkRVGSAUgzE0xl2=0%9NpSp*Sq0~9O4sH8;^P&BMrhzc4OE43hy zq?N6e&5CUly(EY`lxWq0Y(Zp~#Rh>ug7>4hKktv3dCxoVnKS3i^UO?ccxa%OhKU9M zKr3j6-yV1ruQoIU_ls{TcmQC;L4K5d48&9)^Thto#!o&~|JM5||E=XC?P3x8dU4)+ zKW_VZ3NL|n>%^bVQBxNV+UOCiP=;z}Jfg%#HZ=PqZoK8#>tpU`AEh{hhq*^rT{*bn)SWN*R_+ZXhR=^3$)JcK#)m&y-S}q=8P= z<1*sr6LD+9X7LNjYg6`FkCO*`?|Z(>tgR|>XTwBMuq{0hqo1_vkK1A%CX|+O>s50ECS1= zHnvTH34aca{QBL+0|C}II?%!&K5LR&z_$ndO}`9ST(@^w537U|8HnPC4xISd`^E^f zq2{9F=E9;p?)TM_LNK4a{Td=T^?Hp)@3G);S&(Cg9m|_}4=pS=?nX&R3&N!AWw+g> zwjgt5POoXNzyJnkaN>i>>%hFc`Xbdm@*PT4KES6M%jq||LjN|{1{UN!xJ~}p4R-}+ zFWoG7WCt+qU%aDnc2Soy82erS+?C|D{k#`zS<#Z;m;f+y3bN=5X=l=u@UewhrF>r| zGx3~Lpl0m)D~h#=&sVu1-A(?lP22YL6Ykx}c#DM@RFJTUBDe-)*^$H6wKN`2hq@K^_?A)Lc=juM zZ`LsEQZszAHI3-anp$hGm3-LPv>%e2?yDV+kr%$+nfbv4Y8Pf8QSY{IU=S@Fr_upX)n=v*^{qL zB{JPG&%5&af8V#N4h|2rsEgR3zlwKCdHpI4mPFZYYd^H5DSvMrFlrZk&B@i59;{!5 z$2c)RzVXjP7XGN1!URb3-hQ>`BqN4l_s_FHXO!`GigAH@ANa6k=7~TH~ zg)WEOwLU@lXpPqs0M4{J2d-Fn1B!m?;i>hjmCi7i9dGg;#4KepZA9?B^c_jk;Y*^l zOecK)-tjepI|VJ6w&6TVEUPBLQebX#&5lBI02_<3dBz5qmqL0y2&97%FQgBw}khuD{RU3NHtM5$Fo;$MPR zC#lIK=16;swu83pGX10viG5R%0#v3qnrr5v`jNj7+&AA3TSJR6BT|(lP7HuLo zv}v8yZ%}thj5;<#_~ub!Jm+J314^^fw2pA)qn=Sl>W?ci#r6iGem;Jqd=eNYA?x~z zx^YKoS>`@byneklKHe1OEr-wtQy(AA7H7fyqFY+C{R_ZWCA0m zbOYL=x{bO5f#=R@l)Qa83Hx-^Yc7{%dXg#`^x@x1dAZ-66>G1c zB~*P<7?s=-4`SDU?RO_zcSlRY8mS~|0h*ULt6{4WFVy5G)5xO)3g^JQ{0RbUmjpj| zO0MKlrvML!xrN~7mf`QI4n->ASE8$P2J#VIJC{36-ehg)#XJJ9vJHg|ke|CW4q`L} zWM??XawK}Y!sXGOk{6#9xb=;Kfvv1?$!-0=j4gx*OUc5sPax*6f*AFjb34awi1Kge z1Rq;L5O&bBP+)Lf=$;dSC7r<_9l0QnMxY~al`bC8Z4(Tb`8>Vlrh=A~ZlL~@$<)sT z>1Zclg}VDccT*Vn=0sr~hcOy|U?z@W?d5U6=YeS#Em-QqZ;Zfqt6_uh z{y@6`4Kd1P2Y(Ly6%xi6ycl=0wy$JI+RwJ!Aqs+R(!r7z&3wkzNBH&T&+XME=R@N3 ze~MWL&T!wHF~wFaagR1!Q1_U?=(D(vf!55=kcQ6@cYfg3(EItjM-lH2<@a>?&L~3g zT`>kOsTb;P&?Ht?_T7?;T|2EXNF8C!=06WS)K0sKp9M?9BEH?*#ysn1SO!pCnEj?I zS!bhSYiVk3W>Yfn>RjSA?|a|S$SsuKq9E%JfK7b z!2zvGDH@|d;#ii!3CD8EgBc!Us#2*KlS(Rsi4UntDnnI91!EL0;J_fre1XewzC83f z1a8ji)xCH3-us;JKb|_h*1y)@ySsOH@9vsuLjYrqF+VZJ3~cY!}Z2lbq;9_8#rwDxo`8_ZP zIHK%0+8V$m0Z&3J>;!%ZTnY3mJD#=#a8br5p)VF=pNaux$5beQ1A*&-?*Xp^uLARc zPY2k@+8njQN5GxHfhpsz1c2ed+d?AN16KvuiuEKk@h{-gl<}7eU<~lFl7u>NOMuM_ zfvpim0NY0#lQI{D0~igg^-Mq=xIMu31qnphNI*rXG=Niqb%6<}1G58sq!0n%2L$v@ zl>=}Z;mL2J4%`~x>wNq~cmbFWEb`>B7#N;1Pe}lrj_p__>;@hN);RuC2W|`Sc^=T? zu=gBLYy7t#a3iqNLxlCfh3T>t8NeAH$-k-b-;vm%pi6|1K4Fp|!ZX7E4{ZpuOA%ow z0hOUNppSNT12^_5GaPqKtOIujaF_sW){@_+%$dNy6%hdL&61Zw0E`7TILPk;W(j3S zpg#$B1aO%MYylPolsO1^SrH(pG8BeJ4bqZ-gJR{O!SAZP~5AS9%Inali zV$E-1YZ$OX34TriF-^iEpVxRQTepv{wkRTU6nw8CREFd{V%7r`%h zB;VQWIBXBa{UiX~7ds|_hiiba3^+VBk5^VZO`8$9wCWs|7fWg30MTD;>@;wUq zDezuWZ5q>-^m>6QDFVq0-~`}3MT8{H5%5#N-&^=jQ2=M-uKATseiOj4K(`{o`~-d{ zIsBdOV_*XA9R6|sq@0B}68yhss%8n(8#!qZyefw-3fdx6O@38#jq2oFW}`BlMB zH?C#~yER0@_I68t6TorUxq27kyf9gRM`1S{w8Ckwf`4-)|MFM_%Ydf{k3{tMJHdzC z`6KVy^jgEMI`A8!J{rl_eS-a8xe7_--k^-7wgkYAjx-S-i{NLBV0#177dhAuB_zDr zVIpWl)a=s-X3usEqXeU3r}VSfG$vNQZpx*$`og z67Q2a>^~vce9Bd*2E3$@TGT-z9F5&}?t+{!w{2U=ZYq^UxbQ?@Pda3 zpDUDDA~ZzMz8ibK3}Vu-s)=_xNQ6N?B2eT$&<3&nBJ7KAmLbC9`$gCX->RwzM-@sc z5gH;aRO0=186s%Y7E6J_g_25y=prD0yBjpw_J1bA;T|G<$4dm{)vvJ`h&`r|TGnAA zyy+o=`xGwV6`^pZPl?|Flpr^Fjphq(W*59diB-etm-c zVK^{bDD+xRV%mE`cL#}}^$B(045{!aq0m}#4yVwEsNKM^d3;m_=zYS=Qo;Vn-}R1; zKuqhHmv)#4ZwPTNaTR<`D0FuuVvl-=FgS*t&Z%hD};%;_& zfb)Hg)v!nPw!*840(j6vgcDNu&I@1)?wa2NoEK{JR>4>u_s@5tPyD#_mhoEyuT>xC)l!Fb4Dlr-^S(INm>dn7;J zND(*~`3#;O61T^=PaOe!X52|BIqbznOMbpK-{JUXhtofh_<9fY(a{QEXkrf2v4>7c z*o@ChRz~OjvBzymNWL!;xXVL?VTpWSQ(E%(BGH9>PDGsoj;8StVP$L)fEh~iHv!{g z`gD=G!$G8%6z$*ta+s%xuo^fek}s{ZdZcu1-_iow)b)*c5slZ zIRb9LUGq0oD)~)d?^$#O5Aa>!pa7c-uRB?Mys{xplMz7W^s=Qz)iUOvGqvX z2&)L}m4UAN!J1fsy(Fz)qg)MpSC@p)C*d|lgf-Y%UM>0hfqGQ~_858X>+&WRVaMFs z=dNIP6!|mY);CF5hwB)zsslI-_ zu^qT9fWwz?H#K{sJ)0dDut&fAJhBK|3Aj8HeRKr2qKrwIi&BBT=UqE*Aq3liivs*l zMIIn5q8+cK4Fzxj_8LI#>ZuSByr(;`ebZdvN5JdY7sQ#s0V(6I9I*GS%);1}pGaV~5JE-5>nwg$F&m<9YSqZn;-fOzErd_y=3_zxq4hNx9T zv4=_o0QjUaX1Fn?%NR4n7;~aAX0S2l5M#^$W6Y + + Date: Mon, 27 Mar 2017 11:26:36 -0300 Subject: [PATCH 4/7] Fix non-commited file --- app/src/main/res/menu/video_player.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/menu/video_player.xml b/app/src/main/res/menu/video_player.xml index 87aaccee8..c79217adc 100644 --- a/app/src/main/res/menu/video_player.xml +++ b/app/src/main/res/menu/video_player.xml @@ -5,5 +5,5 @@ + android:icon="@drawable/ic_screen_rotation_white"/> \ No newline at end of file From 7a4a54c3ea1fc3f12a49d0bd228ea982614bb911 Mon Sep 17 00:00:00 2001 From: Mauricio Colli Date: Mon, 27 Mar 2017 16:34:37 -0300 Subject: [PATCH 5/7] Fix travis - Remove duplicate of AndroidManifest - Remove some non-translatable strings from "ar" translation, and general clean-up of other --- .travis.yml | 22 +-- app/build.gradle | 6 +- app/src/main/AndroidManifest.xml | 5 - app/src/main/res/layout/content_channel.xml | 17 --- app/src/main/res/values-ar/strings.xml | 49 ------- app/src/main/res/values-cs/strings.xml | 139 ------------------- app/src/main/res/values-de/strings.xml | 141 ------------------- app/src/main/res/values-el/strings.xml | 2 - app/src/main/res/values-eo/strings.xml | 2 - app/src/main/res/values-es/strings.xml | 141 ------------------- app/src/main/res/values-eu/strings.xml | 3 - app/src/main/res/values-fr/strings.xml | 4 - app/src/main/res/values-hu/strings.xml | 4 - app/src/main/res/values-id/strings.xml | 137 ------------------- app/src/main/res/values-it/strings.xml | 143 -------------------- app/src/main/res/values-ja/strings.xml | 141 ------------------- app/src/main/res/values-ko/strings.xml | 6 - app/src/main/res/values-nb-rNO/strings.xml | 2 - app/src/main/res/values-nl/strings.xml | 141 ------------------- app/src/main/res/values-pt/strings.xml | 142 ------------------- app/src/main/res/values-ro/strings.xml | 2 - app/src/main/res/values-ru/strings.xml | 4 - app/src/main/res/values-sk/strings.xml | 139 ------------------- app/src/main/res/values-sl/strings.xml | 4 - app/src/main/res/values-sr/strings.xml | 141 ------------------- app/src/main/res/values-tr/strings.xml | 1 - app/src/main/res/values-zh-rCN/strings.xml | 2 - app/src/main/res/values-zh-rHK/strings.xml | 2 - app/src/main/res/values-zh-rTW/strings.xml | 2 - app/src/main/res/values/strings.xml | 89 ------------ app/src/main/res/values/styles.xml | 7 - 31 files changed, 4 insertions(+), 1636 deletions(-) delete mode 100644 app/src/main/res/layout/content_channel.xml diff --git a/.travis.yml b/.travis.yml index 36b71b8ca..3df95873e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,29 +11,9 @@ android: - android-25 # Additional components - - extra-android-support - extra-android-m2repository - - extra-google-m2repository - # Emulators - - sys-img-armeabi-v7a-android-21 - - sys-img-armeabi-v7a-android-19 - - sys-img-armeabi-v7a-android-15 - -env: - global: - - ADB_INSTALL_TIMEOUT=8 # minutes (2 by default) - - GRADLE_OPTS=-Xmx512m # give gradle more memory since it seem to fail otherwise - matrix: - - ANDROID_TARGET=android-21 ANDROID_ABI=armeabi-v7a - -before_script: - - echo no | android create avd --force -n test -t $ANDROID_TARGET --abi $ANDROID_ABI - - emulator -avd test -no-skin -no-audio -no-window & - - android-wait-for-emulator - - adb shell input keyevent 82 & - -script: ./gradlew --info build connectedCheck +script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug testDebugUnitTest licenses: - '.+' diff --git a/app/build.gradle b/app/build.gradle index 0f99854c2..52b2c673f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -32,6 +32,9 @@ android { dependencies { testCompile 'junit:junit:4.12' + testCompile 'org.mockito:mockito-core:1.10.19' + testCompile 'org.json:json:20160810' + compile 'com.android.support:appcompat-v7:25.1.0' compile 'com.android.support:support-v4:25.1.0' compile 'com.android.support:design:25.1.0' @@ -46,7 +49,4 @@ dependencies { compile 'com.nononsenseapps:filepicker:3.0.0' compile 'ch.acra:acra:4.9.0' compile 'com.google.android.exoplayer:exoplayer:r2.3.1' - testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:1.10.19' - testCompile 'org.json:json:20160810' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3172506f9..0cf26b394 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -53,11 +53,6 @@ android:launchMode="singleInstance" android:theme="@style/PlayerTheme"/> - - diff --git a/app/src/main/res/layout/content_channel.xml b/app/src/main/res/layout/content_channel.xml deleted file mode 100644 index 79a41a628..000000000 --- a/app/src/main/res/layout/content_channel.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 7e2e2d82e..e10406726 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -1,27 +1,5 @@ - Navigate home - %1$s, %2$s - %1$s, %2$s, %3$s - Navigate up - More options - Done - See all - Choose an app - OFF - ON - Search… - Clear query - Search query - Search - Submit query - Voice search - Share with - Share with %s - Collapse - 999+ - "بدء تشغيل الفيديو تلقائيًا عندما يتم فتحه من تطبيق أخر." - التشغيل التلقائي مشغل NewPipe في الخلفية جاري التشغيل في الخلفية إلغاء @@ -87,31 +65,4 @@ خطأ لا يمكن تحليل الموقع. لا يمكن فك تشفير توقيع رابط الفيديو. - NewPipe - android.support.design.widget.AppBarLayout$ScrollingViewBehavior - autoplay_through_intent - %1$s - NewPipe - https://www.c3s.cc/ - %1$d / %2$d - default_audio_format - m4a - en - default_resolution_preference - 360p - 0 - download_path_audio - download_path - https://f-droid.org/repository/browse/?fdfilter=Kore&fdid=org.xbmc.kore - https://f-droid.org/repository/browse/?fdfilter=vlc&fdid=org.videolan.vlc - search_language - settings_category_appearance - settings_category_other - settings_category_video_audio - show_next_video - show_play_with_kodi - الثيمات - NewPipe - use_external_audio_player - use_external_video_player - use_tor diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 9a0f3690e..95164d622 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -22,8 +22,6 @@ Zadejte umístění pro stažené audio soubory. Umístění pro stažené audio - Automatické přehrávání skrze Intent - Automaticky přehrávat video, jestliže je volané z jiné aplikace. Výchozí rozlišení Přehrát pomocí Kodi Aplikace Kore nenalezena. Nainstalovat Kore? @@ -159,143 +157,6 @@ Aktivita kanálu Hlášení uživatele - " - Material is the metaphor. - - - - A material metaphor is the unifying theory of a rationalized space and a system of motion. - The material is grounded in tactile reality, inspired by the study of paper and ink, yet - technologically advanced and open to imagination and magic. - - Surfaces and edges of the material provide visual cues that are grounded in reality. The - use of familiar tactile attributes helps users quickly understand affordances. Yet the - flexibility of the material creates new affordances that supercede those in the physical - world, without breaking the rules of physics. - - The fundamentals of light, surface, and movement are key to conveying how objects move, - interact, and exist in space and in relation to each other. Realistic lighting shows - seams, divides space, and indicates moving parts. - - - - Bold, graphic, intentional. - - - - The foundational elements of print based design typography, grids, space, scale, color, - and use of imagery guide visual treatments. These elements do far more than please the - eye. They create hierarchy, meaning, and focus. Deliberate color choices, edge to edge - imagery, large scale typography, and intentional white space create a bold and graphic - interface that immerse the user in the experience. - - An emphasis on user actions makes core functionality immediately apparent and provides - waypoints for the user. - - - - Motion provides meaning. - - - - Motion respects and reinforces the user as the prime mover. Primary user actions are - inflection points that initiate motion, transforming the whole design. - - All action takes place in a single environment. Objects are presented to the user without - breaking the continuity of experience even as they transform and reorganize. - - Motion is meaningful and appropriate, serving to focus attention and maintain continuity. - Feedback is subtle yet clear. Transitions are efficient yet coherent. - - - - 3D world. - - - - The material environment is a 3D space, which means all objects have x, y, and z - dimensions. The z-axis is perpendicularly aligned to the plane of the display, with the - positive z-axis extending towards the viewer. Every sheet of material occupies a single - position along the z-axis and has a standard 1dp thickness. - - On the web, the z-axis is used for layering and not for perspective. The 3D world is - emulated by manipulating the y-axis. - - - - Light and shadow. - - - - Within the material environment, virtual lights illuminate the scene. Key lights create - directional shadows, while ambient light creates soft shadows from all angles. - - Shadows in the material environment are cast by these two light sources. In Android - development, shadows occur when light sources are blocked by sheets of material at - various positions along the z-axis. On the web, shadows are depicted by manipulating the - y-axis only. The following example shows the card with a height of 6dp. - - - - Resting elevation. - - - - All material objects, regardless of size, have a resting elevation, or default elevation - that does not change. If an object changes elevation, it should return to its resting - elevation as soon as possible. - - - - Component elevations. - - - - The resting elevation for a component type is consistent across apps (e.g., FAB elevation - does not vary from 6dp in one app to 16dp in another app). - - Components may have different resting elevations across platforms, depending on the depth - of the environment (e.g., TV has a greater depth than mobile or desktop). - - - - Responsive elevation and dynamic elevation offsets. - - - - Some component types have responsive elevation, meaning they change elevation in response - to user input (e.g., normal, focused, and pressed) or system events. These elevation - changes are consistently implemented using dynamic elevation offsets. - - Dynamic elevation offsets are the goal elevation that a component moves towards, relative - to the component’s resting state. They ensure that elevation changes are consistent - across actions and component types. For example, all components that lift on press have - the same elevation change relative to their resting elevation. - - Once the input event is completed or cancelled, the component will return to its resting - elevation. - - - - Avoiding elevation interference. - - - - Components with responsive elevations may encounter other components as they move between - their resting elevations and dynamic elevation offsets. Because material cannot pass - through other material, components avoid interfering with one another any number of ways, - whether on a per component basis or using the entire app layout. - - On a component level, components can move or be removed before they cause interference. - For example, a floating action button (FAB) can disappear or move off screen before a - user picks up a card, or it can move if a snackbar appears. - - On the layout level, design your app layout to minimize opportunities for interference. - For example, position the FAB to one side of stream of a cards so the FAB won’t interfere - when a user tries to pick up one of cards. - - - " Podrobné Nelze zjistit dekodéry zařízení Nelze doložit dekodér diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d5330ad9e..01e415eaa 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -16,16 +16,12 @@ Browser Rotation Einstellungen - Externen Player benutzen Downloadverzeichnis für Videos Verzeichnis in dem heruntergeladene Videos gespeichert werden. Downloadverzeichnis für Videos eingeben - Automatisches Abspielen durch Intent - Startet ein Video automatisch wenn es von einer anderen App aufgerufen wurde. Standardauflösung Mit Kodi abspielen Kore App wurde nicht gefunden. Möchten sie Kore jetzt installieren? - Kore installieren Zeige \"Mit Kodi abspielen\" Option Zeigt eine Option an, über die man Videos mit dem Kodi Mediacenter abspielen kann. Audio @@ -169,143 +165,6 @@ Beschädigte URL oder Internet nicht erreichbar Vorgeschlagener Dateiname Kanalaktivität - " - Material is the metaphor. - - - - A material metaphor is the unifying theory of a rationalized space and a system of motion. - The material is grounded in tactile reality, inspired by the study of paper and ink, yet - technologically advanced and open to imagination and magic. - - Surfaces and edges of the material provide visual cues that are grounded in reality. The - use of familiar tactile attributes helps users quickly understand affordances. Yet the - flexibility of the material creates new affordances that supercede those in the physical - world, without breaking the rules of physics. - - The fundamentals of light, surface, and movement are key to conveying how objects move, - interact, and exist in space and in relation to each other. Realistic lighting shows - seams, divides space, and indicates moving parts. - - - - Bold, graphic, intentional. - - - - The foundational elements of print based design typography, grids, space, scale, color, - and use of imagery guide visual treatments. These elements do far more than please the - eye. They create hierarchy, meaning, and focus. Deliberate color choices, edge to edge - imagery, large scale typography, and intentional white space create a bold and graphic - interface that immerse the user in the experience. - - An emphasis on user actions makes core functionality immediately apparent and provides - waypoints for the user. - - - - Motion provides meaning. - - - - Motion respects and reinforces the user as the prime mover. Primary user actions are - inflection points that initiate motion, transforming the whole design. - - All action takes place in a single environment. Objects are presented to the user without - breaking the continuity of experience even as they transform and reorganize. - - Motion is meaningful and appropriate, serving to focus attention and maintain continuity. - Feedback is subtle yet clear. Transitions are efficient yet coherent. - - - - 3D world. - - - - The material environment is a 3D space, which means all objects have x, y, and z - dimensions. The z-axis is perpendicularly aligned to the plane of the display, with the - positive z-axis extending towards the viewer. Every sheet of material occupies a single - position along the z-axis and has a standard 1dp thickness. - - On the web, the z-axis is used for layering and not for perspective. The 3D world is - emulated by manipulating the y-axis. - - - - Light and shadow. - - - - Within the material environment, virtual lights illuminate the scene. Key lights create - directional shadows, while ambient light creates soft shadows from all angles. - - Shadows in the material environment are cast by these two light sources. In Android - development, shadows occur when light sources are blocked by sheets of material at - various positions along the z-axis. On the web, shadows are depicted by manipulating the - y-axis only. The following example shows the card with a height of 6dp. - - - - Resting elevation. - - - - All material objects, regardless of size, have a resting elevation, or default elevation - that does not change. If an object changes elevation, it should return to its resting - elevation as soon as possible. - - - - Component elevations. - - - - The resting elevation for a component type is consistent across apps (e.g., FAB elevation - does not vary from 6dp in one app to 16dp in another app). - - Components may have different resting elevations across platforms, depending on the depth - of the environment (e.g., TV has a greater depth than mobile or desktop). - - - - Responsive elevation and dynamic elevation offsets. - - - - Some component types have responsive elevation, meaning they change elevation in response - to user input (e.g., normal, focused, and pressed) or system events. These elevation - changes are consistently implemented using dynamic elevation offsets. - - Dynamic elevation offsets are the goal elevation that a component moves towards, relative - to the component’s resting state. They ensure that elevation changes are consistent - across actions and component types. For example, all components that lift on press have - the same elevation change relative to their resting elevation. - - Once the input event is completed or cancelled, the component will return to its resting - elevation. - - - - Avoiding elevation interference. - - - - Components with responsive elevations may encounter other components as they move between - their resting elevations and dynamic elevation offsets. Because material cannot pass - through other material, components avoid interfering with one another any number of ways, - whether on a per component basis or using the entire app layout. - - On a component level, components can move or be removed before they cause interference. - For example, a floating action button (FAB) can disappear or move off screen before a - user picks up a card, or it can move if a snackbar appears. - - On the layout level, design your app layout to minimize opportunities for interference. - For example, position the FAB to one side of stream of a cards so the FAB won’t interfere - when a user tries to pick up one of cards. - - - " reCAPTCHA Schwarz diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 638feaef9..1ee429ac5 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -28,8 +28,6 @@ Διαδρομή για αποθήκευση αρχείων ήχου Εισάγετε διαδρομή για λήψη αρχείων ήχου. - Αυτόματη αναπαραγωγή μέσω Intent - Αυτόματη αναπαραγωγή video όταν καλείται από άλλη εφαρμογή. Προεπιλεγμένη ανάλυση Αναπαραγωγή με το Kodi Η εφαρμογή Kore δεν βρέθηκε. Εγκατάσταση; diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 50089bb9c..cb852e61a 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -48,7 +48,6 @@ Ŝatoj Malŝatoj Uzi la programon Tor - Ludi aŭtomate per Intent Neniu elsendlflua ludilo trovita. Ĉu instali la aplikaĵon VLC? La aplikaĵo Kore ne estas trovita. Ĉu instali la aplikaĵon Kore? Montri la sekvan videon kaj similajn videojn @@ -67,7 +66,6 @@ Montri opcion por ludi videon per la aplikaĵo Kodi. Dosierujo por konservi elŝutitajn videojn. Dosierujo por konservi elŝutitan muzikon - Ludi videon aŭtomate kiam ĝi estas vokita de alia aplikaĵo. Elektu lokon por konservi elŝutitajn videojn Elektu lokon por konservi elŝutitan muzikon. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 0d3f92648..658fb06f0 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -16,16 +16,12 @@ Seleccionar navegador rotación Ajustes - Usar reproductor externo Ruta de descarga de vídeo Ruta para almacenar los vídeos descargados. Introducir directorio de descargas para vídeos - Intentar reproducción automática - Reproducir vídeos automáticamente cuando se llamen desde otra aplicación. Resolución por defecto Reproducir con Kodi Aplicación Kore no encontrada. ¿Instalar Kore? - Instalar Kore Mostrar opción \"Reproducir con Kodi\" Muestra una opción para reproducir el vídeo con Kodi Media Center. Audio @@ -159,143 +155,6 @@ Actividad del canal Ajustes - " - Material is the metaphor. - - - - A material metaphor is the unifying theory of a rationalized space and a system of motion. - The material is grounded in tactile reality, inspired by the study of paper and ink, yet - technologically advanced and open to imagination and magic. - - Surfaces and edges of the material provide visual cues that are grounded in reality. The - use of familiar tactile attributes helps users quickly understand affordances. Yet the - flexibility of the material creates new affordances that supercede those in the physical - world, without breaking the rules of physics. - - The fundamentals of light, surface, and movement are key to conveying how objects move, - interact, and exist in space and in relation to each other. Realistic lighting shows - seams, divides space, and indicates moving parts. - - - - Bold, graphic, intentional. - - - - The foundational elements of print based design typography, grids, space, scale, color, - and use of imagery guide visual treatments. These elements do far more than please the - eye. They create hierarchy, meaning, and focus. Deliberate color choices, edge to edge - imagery, large scale typography, and intentional white space create a bold and graphic - interface that immerse the user in the experience. - - An emphasis on user actions makes core functionality immediately apparent and provides - waypoints for the user. - - - - Motion provides meaning. - - - - Motion respects and reinforces the user as the prime mover. Primary user actions are - inflection points that initiate motion, transforming the whole design. - - All action takes place in a single environment. Objects are presented to the user without - breaking the continuity of experience even as they transform and reorganize. - - Motion is meaningful and appropriate, serving to focus attention and maintain continuity. - Feedback is subtle yet clear. Transitions are efficient yet coherent. - - - - 3D world. - - - - The material environment is a 3D space, which means all objects have x, y, and z - dimensions. The z-axis is perpendicularly aligned to the plane of the display, with the - positive z-axis extending towards the viewer. Every sheet of material occupies a single - position along the z-axis and has a standard 1dp thickness. - - On the web, the z-axis is used for layering and not for perspective. The 3D world is - emulated by manipulating the y-axis. - - - - Light and shadow. - - - - Within the material environment, virtual lights illuminate the scene. Key lights create - directional shadows, while ambient light creates soft shadows from all angles. - - Shadows in the material environment are cast by these two light sources. In Android - development, shadows occur when light sources are blocked by sheets of material at - various positions along the z-axis. On the web, shadows are depicted by manipulating the - y-axis only. The following example shows the card with a height of 6dp. - - - - Resting elevation. - - - - All material objects, regardless of size, have a resting elevation, or default elevation - that does not change. If an object changes elevation, it should return to its resting - elevation as soon as possible. - - - - Component elevations. - - - - The resting elevation for a component type is consistent across apps (e.g., FAB elevation - does not vary from 6dp in one app to 16dp in another app). - - Components may have different resting elevations across platforms, depending on the depth - of the environment (e.g., TV has a greater depth than mobile or desktop). - - - - Responsive elevation and dynamic elevation offsets. - - - - Some component types have responsive elevation, meaning they change elevation in response - to user input (e.g., normal, focused, and pressed) or system events. These elevation - changes are consistently implemented using dynamic elevation offsets. - - Dynamic elevation offsets are the goal elevation that a component moves towards, relative - to the component’s resting state. They ensure that elevation changes are consistent - across actions and component types. For example, all components that lift on press have - the same elevation change relative to their resting elevation. - - Once the input event is completed or cancelled, the component will return to its resting - elevation. - - - - Avoiding elevation interference. - - - - Components with responsive elevations may encounter other components as they move between - their resting elevations and dynamic elevation offsets. Because material cannot pass - through other material, components avoid interfering with one another any number of ways, - whether on a per component basis or using the entire app layout. - - On a component level, components can move or be removed before they cause interference. - For example, a floating action button (FAB) can disappear or move off screen before a - user picks up a card, or it can move if a snackbar appears. - - On the layout level, design your app layout to minimize opportunities for interference. - For example, position the FAB to one side of stream of a cards so the FAB won’t interfere - when a user tries to pick up one of cards. - - - " Oscuro Todo diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 5f0a9f5f7..bfedaf4d6 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -19,7 +19,6 @@ Lehenetsitako bereizmena Kodirekin erreproduzitu Kore aplikazioa ez da aurkitu. Kore beharrezkoa da Kodi multimedia zentroarekin bideoak erreproduzitzeko. - Kore instalatu \"Kodirekin erreproduzitu\" aukera erakutsi Kodi multimedia zentroarekin bideoa erreproduzitzeko aukera erakusten du. Audioa @@ -48,8 +47,6 @@ "Orrialdea bilatu: " Kanpoko bideo erreproduzitzailea erabili Kanpoko audio erreproduzitzailea erabili - Intent bidez automatikoki erreproduzitu - Bideoa automatikoki hasten du beste aplikazio batetik deitu denean. Atzeko planoan erreproduzitzen Ukitu bilaketa hasteko Audioa deskargatzeko kokapena diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index cad6350ff..b9f368548 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1,6 +1,5 @@ - Lire automatiquement une vidéo lorsqu’elle a été appelée depuis une autre application. Annuler Choisir un navigateur  Définition par défaut @@ -10,11 +9,9 @@ Entrer le chemin de téléchargement des vidéos Chemin de stockage des vidéos téléchargées. Installer - Installer Kore L’application Kore est introuvable. Installer Kore ? Aucun lecteur de flux réseau trouvé. Installer VLC ? Ouvrir dans le navigateur - Lecture automatique via Intent Lire avec Kodi rotation Rechercher @@ -26,7 +23,6 @@ Afficher l’option « Lire avec Kodi » Paramètres Ajoutée le %1$s - Utiliser un lecteur externe %1$s vues Audio Format audio par défaut diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index e161f511e..6078fa29c 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -16,16 +16,12 @@ Válasszon böngészőt forgatás Beállítások - Külső lejátszó használata Videófájlok letöltési helye Útvonal a letöltött videók tárolásához. Adja meg a videófájlok letöltési helyét - Automatikus lejátszás Intent-en keresztül - Videó automatikus lejátszása külső alkalmazással való megnyitás esetén. Alapértelmezett felbontás Lejátszás Kodi-val A Kore alkalmazás nem található. Feltelepíti a Kore lejátszót? - Kore telepítése \"Lejátszás Kodi-val\" opció mutatása Mutat egy opciót a videók Kodi médiaközponttal való lejátszására. Hang diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 8f9dfc41f..2c14f099f 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -151,143 +151,6 @@ Laporan pengguna Thread - " - Material is the metaphor. - - - - A material metaphor is the unifying theory of a rationalized space and a system of motion. - The material is grounded in tactile reality, inspired by the study of paper and ink, yet - technologically advanced and open to imagination and magic. - - Surfaces and edges of the material provide visual cues that are grounded in reality. The - use of familiar tactile attributes helps users quickly understand affordances. Yet the - flexibility of the material creates new affordances that supercede those in the physical - world, without breaking the rules of physics. - - The fundamentals of light, surface, and movement are key to conveying how objects move, - interact, and exist in space and in relation to each other. Realistic lighting shows - seams, divides space, and indicates moving parts. - - - - Bold, graphic, intentional. - - - - The foundational elements of print based design typography, grids, space, scale, color, - and use of imagery guide visual treatments. These elements do far more than please the - eye. They create hierarchy, meaning, and focus. Deliberate color choices, edge to edge - imagery, large scale typography, and intentional white space create a bold and graphic - interface that immerse the user in the experience. - - An emphasis on user actions makes core functionality immediately apparent and provides - waypoints for the user. - - - - Motion provides meaning. - - - - Motion respects and reinforces the user as the prime mover. Primary user actions are - inflection points that initiate motion, transforming the whole design. - - All action takes place in a single environment. Objects are presented to the user without - breaking the continuity of experience even as they transform and reorganize. - - Motion is meaningful and appropriate, serving to focus attention and maintain continuity. - Feedback is subtle yet clear. Transitions are efficient yet coherent. - - - - 3D world. - - - - The material environment is a 3D space, which means all objects have x, y, and z - dimensions. The z-axis is perpendicularly aligned to the plane of the display, with the - positive z-axis extending towards the viewer. Every sheet of material occupies a single - position along the z-axis and has a standard 1dp thickness. - - On the web, the z-axis is used for layering and not for perspective. The 3D world is - emulated by manipulating the y-axis. - - - - Light and shadow. - - - - Within the material environment, virtual lights illuminate the scene. Key lights create - directional shadows, while ambient light creates soft shadows from all angles. - - Shadows in the material environment are cast by these two light sources. In Android - development, shadows occur when light sources are blocked by sheets of material at - various positions along the z-axis. On the web, shadows are depicted by manipulating the - y-axis only. The following example shows the card with a height of 6dp. - - - - Resting elevation. - - - - All material objects, regardless of size, have a resting elevation, or default elevation - that does not change. If an object changes elevation, it should return to its resting - elevation as soon as possible. - - - - Component elevations. - - - - The resting elevation for a component type is consistent across apps (e.g., FAB elevation - does not vary from 6dp in one app to 16dp in another app). - - Components may have different resting elevations across platforms, depending on the depth - of the environment (e.g., TV has a greater depth than mobile or desktop). - - - - Responsive elevation and dynamic elevation offsets. - - - - Some component types have responsive elevation, meaning they change elevation in response - to user input (e.g., normal, focused, and pressed) or system events. These elevation - changes are consistently implemented using dynamic elevation offsets. - - Dynamic elevation offsets are the goal elevation that a component moves towards, relative - to the component’s resting state. They ensure that elevation changes are consistent - across actions and component types. For example, all components that lift on press have - the same elevation change relative to their resting elevation. - - Once the input event is completed or cancelled, the component will return to its resting - elevation. - - - - Avoiding elevation interference. - - - - Components with responsive elevations may encounter other components as they move between - their resting elevations and dynamic elevation offsets. Because material cannot pass - through other material, components avoid interfering with one another any number of ways, - whether on a per component basis or using the entire app layout. - - On a component level, components can move or be removed before they cause interference. - For example, a floating action button (FAB) can disappear or move off screen before a - user picks up a card, or it can move if a snackbar appears. - - On the layout level, design your app layout to minimize opportunities for interference. - For example, position the FAB to one side of stream of a cards so the FAB won’t interfere - when a user tries to pick up one of cards. - - - " reCAPTCHA Rintangan reCAPTCHA diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 302dfd7f1..30f171f22 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -16,16 +16,12 @@ Scegli browser rotazione Impostazioni - Usa un riproduttore video esterno Cartella dei video scaricati Percorso dove memorizzare i video scaricati. Inserisci il percorso per i download - Auto riproduzione attraverso internet - Avvia automaticamente un video quando è stato chiamato da un\'altra applicazione. Risoluzione predefinita Riproduci con Kodi L\'applicazione Kore non è stata trovata. Kore è necessario per riprodurre video con Kodi media center. Vorresti installarlo? - Installa Kore Mostra l\'opzione \"Riproduci con Kodi\" Mostra un opzione per riprodurre un video attraverso Kodi media center. Audio @@ -39,8 +35,6 @@ Video simili Lingua preferita per i contenuti Video e Audio - INFO - ETC Anteprima video Anteprima video @@ -164,143 +158,6 @@ Impostazioni ChannelActivity - " - Material is the metaphor. - - - - A material metaphor is the unifying theory of a rationalized space and a system of motion. - The material is grounded in tactile reality, inspired by the study of paper and ink, yet - technologically advanced and open to imagination and magic. - - Surfaces and edges of the material provide visual cues that are grounded in reality. The - use of familiar tactile attributes helps users quickly understand affordances. Yet the - flexibility of the material creates new affordances that supercede those in the physical - world, without breaking the rules of physics. - - The fundamentals of light, surface, and movement are key to conveying how objects move, - interact, and exist in space and in relation to each other. Realistic lighting shows - seams, divides space, and indicates moving parts. - - - - Bold, graphic, intentional. - - - - The foundational elements of print based design typography, grids, space, scale, color, - and use of imagery guide visual treatments. These elements do far more than please the - eye. They create hierarchy, meaning, and focus. Deliberate color choices, edge to edge - imagery, large scale typography, and intentional white space create a bold and graphic - interface that immerse the user in the experience. - - An emphasis on user actions makes core functionality immediately apparent and provides - waypoints for the user. - - - - Motion provides meaning. - - - - Motion respects and reinforces the user as the prime mover. Primary user actions are - inflection points that initiate motion, transforming the whole design. - - All action takes place in a single environment. Objects are presented to the user without - breaking the continuity of experience even as they transform and reorganize. - - Motion is meaningful and appropriate, serving to focus attention and maintain continuity. - Feedback is subtle yet clear. Transitions are efficient yet coherent. - - - - 3D world. - - - - The material environment is a 3D space, which means all objects have x, y, and z - dimensions. The z-axis is perpendicularly aligned to the plane of the display, with the - positive z-axis extending towards the viewer. Every sheet of material occupies a single - position along the z-axis and has a standard 1dp thickness. - - On the web, the z-axis is used for layering and not for perspective. The 3D world is - emulated by manipulating the y-axis. - - - - Light and shadow. - - - - Within the material environment, virtual lights illuminate the scene. Key lights create - directional shadows, while ambient light creates soft shadows from all angles. - - Shadows in the material environment are cast by these two light sources. In Android - development, shadows occur when light sources are blocked by sheets of material at - various positions along the z-axis. On the web, shadows are depicted by manipulating the - y-axis only. The following example shows the card with a height of 6dp. - - - - Resting elevation. - - - - All material objects, regardless of size, have a resting elevation, or default elevation - that does not change. If an object changes elevation, it should return to its resting - elevation as soon as possible. - - - - Component elevations. - - - - The resting elevation for a component type is consistent across apps (e.g., FAB elevation - does not vary from 6dp in one app to 16dp in another app). - - Components may have different resting elevations across platforms, depending on the depth - of the environment (e.g., TV has a greater depth than mobile or desktop). - - - - Responsive elevation and dynamic elevation offsets. - - - - Some component types have responsive elevation, meaning they change elevation in response - to user input (e.g., normal, focused, and pressed) or system events. These elevation - changes are consistently implemented using dynamic elevation offsets. - - Dynamic elevation offsets are the goal elevation that a component moves towards, relative - to the component’s resting state. They ensure that elevation changes are consistent - across actions and component types. For example, all components that lift on press have - the same elevation change relative to their resting elevation. - - Once the input event is completed or cancelled, the component will return to its resting - elevation. - - - - Avoiding elevation interference. - - - - Components with responsive elevations may encounter other components as they move between - their resting elevations and dynamic elevation offsets. Because material cannot pass - through other material, components avoid interfering with one another any number of ways, - whether on a per component basis or using the entire app layout. - - On a component level, components can move or be removed before they cause interference. - For example, a floating action button (FAB) can disappear or move off screen before a - user picks up a card, or it can move if a snackbar appears. - - On the layout level, design your app layout to minimize opportunities for interference. - For example, position the FAB to one side of stream of a cards so the FAB won’t interfere - when a user tries to pick up one of cards. - - - " reCAPTCHA Sfida reCAPTCHA diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 9fb0d33e1..17c9f8aad 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -15,16 +15,12 @@ ブラウザーを選択 回転 設定 - 外部プレーヤーを使用する 動画を保存する場所 動画を保存する位置 動画を保存する場所を入力して下さい - インテントで自動再生 - 他のアプリケーションから呼び出されたとき、自動的に動画再生を開始します。 基本解像度 Kodi で再生 Koreが見つかりません。Kore を入手しますか。 - Kore をインストール \"Kodi で再生\" 設定を表示 Kodi メディアセンター経由で動画を再生するための設定を表示します 音楽 @@ -162,143 +158,6 @@ アプリ/UI がクラッシュしました 何:\\n提案:\\nコンテンツ言語:\\nサービス:\\nGMT 時間:\\nパッケージ:\\nバージョン:\\nOS バージョン:\\nグローバル IP 範囲: - " - Material is the metaphor. - - - - A material metaphor is the unifying theory of a rationalized space and a system of motion. - The material is grounded in tactile reality, inspired by the study of paper and ink, yet - technologically advanced and open to imagination and magic. - - Surfaces and edges of the material provide visual cues that are grounded in reality. The - use of familiar tactile attributes helps users quickly understand affordances. Yet the - flexibility of the material creates new affordances that supercede those in the physical - world, without breaking the rules of physics. - - The fundamentals of light, surface, and movement are key to conveying how objects move, - interact, and exist in space and in relation to each other. Realistic lighting shows - seams, divides space, and indicates moving parts. - - - - Bold, graphic, intentional. - - - - The foundational elements of print based design typography, grids, space, scale, color, - and use of imagery guide visual treatments. These elements do far more than please the - eye. They create hierarchy, meaning, and focus. Deliberate color choices, edge to edge - imagery, large scale typography, and intentional white space create a bold and graphic - interface that immerse the user in the experience. - - An emphasis on user actions makes core functionality immediately apparent and provides - waypoints for the user. - - - - Motion provides meaning. - - - - Motion respects and reinforces the user as the prime mover. Primary user actions are - inflection points that initiate motion, transforming the whole design. - - All action takes place in a single environment. Objects are presented to the user without - breaking the continuity of experience even as they transform and reorganize. - - Motion is meaningful and appropriate, serving to focus attention and maintain continuity. - Feedback is subtle yet clear. Transitions are efficient yet coherent. - - - - 3D world. - - - - The material environment is a 3D space, which means all objects have x, y, and z - dimensions. The z-axis is perpendicularly aligned to the plane of the display, with the - positive z-axis extending towards the viewer. Every sheet of material occupies a single - position along the z-axis and has a standard 1dp thickness. - - On the web, the z-axis is used for layering and not for perspective. The 3D world is - emulated by manipulating the y-axis. - - - - Light and shadow. - - - - Within the material environment, virtual lights illuminate the scene. Key lights create - directional shadows, while ambient light creates soft shadows from all angles. - - Shadows in the material environment are cast by these two light sources. In Android - development, shadows occur when light sources are blocked by sheets of material at - various positions along the z-axis. On the web, shadows are depicted by manipulating the - y-axis only. The following example shows the card with a height of 6dp. - - - - Resting elevation. - - - - All material objects, regardless of size, have a resting elevation, or default elevation - that does not change. If an object changes elevation, it should return to its resting - elevation as soon as possible. - - - - Component elevations. - - - - The resting elevation for a component type is consistent across apps (e.g., FAB elevation - does not vary from 6dp in one app to 16dp in another app). - - Components may have different resting elevations across platforms, depending on the depth - of the environment (e.g., TV has a greater depth than mobile or desktop). - - - - Responsive elevation and dynamic elevation offsets. - - - - Some component types have responsive elevation, meaning they change elevation in response - to user input (e.g., normal, focused, and pressed) or system events. These elevation - changes are consistently implemented using dynamic elevation offsets. - - Dynamic elevation offsets are the goal elevation that a component moves towards, relative - to the component’s resting state. They ensure that elevation changes are consistent - across actions and component types. For example, all components that lift on press have - the same elevation change relative to their resting elevation. - - Once the input event is completed or cancelled, the component will return to its resting - elevation. - - - - Avoiding elevation interference. - - - - Components with responsive elevations may encounter other components as they move between - their resting elevations and dynamic elevation offsets. Because material cannot pass - through other material, components avoid interfering with one another any number of ways, - whether on a per component basis or using the entire app layout. - - On a component level, components can move or be removed before they cause interference. - For example, a floating action button (FAB) can disappear or move off screen before a - user picks up a card, or it can move if a snackbar appears. - - On the layout level, design your app layout to minimize opportunities for interference. - For example, position the FAB to one side of stream of a cards so the FAB won’t interfere - when a user tries to pick up one of cards. - - - " reCAPTCHA reCAPTCHA の要求 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 50709b3b3..855db8f26 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -16,16 +16,12 @@ 브라우저 선택 회전 설정 - 외부 플레이어 사용 비디오 다운로드 위치 다운로드된 비디오가 저장될 경로를 선택하세요. 비디오 다운로드 경로 입력 - 인텐트로 경유할 경우 자동 재생 - 다른 앱으로부터 호출되었을 경우에 비디오를 자동으로 재생합니다. 기본 해상도 Kodi로 재생 Kore 앱이 발견되지 않았습니다. Kore를 설치할까요? - Kore 설치 \"Kodi로 재생\" 옵션 표시 비디오를 Kodi media center를 사용해 재생하는 옵션을 표시합니다. 오디오 @@ -39,8 +35,6 @@ 유사한 비디오 선호하는 컨텐츠 언어 비디오 & 오디오 - 정보 - 기타 비디오 미리보기 썸네일 비디오 미리보기 썸네일 diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 6315b61fd..f5befe193 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -28,7 +28,6 @@ Sti å lagre nedlastet lyd i. Skriv inn nedlastingssti for lydfiler. - Automatisk avspilling av video når det blir forespurt fra et annet program. Forvalgt oppløsning Spill av med Kodi Kore-programmet ble ikke funnet. Installer Kore? @@ -65,7 +64,6 @@ Kan ikke opprette nedlastingsmappe \'%1$s\' Opprettet nedlastingsmappen \'%1$s\' -Automatisk avspilling med Intent Trykk for å komme i gang Automatisk avspilling når forespurt av et annet program Automatisk avspilling av video når NewPipe blir forespurt av et annet program. diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 1d1e8a4f0..1e0d80673 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -16,16 +16,12 @@ Kies een browser rotatie Instellingen - Gebruik externe speler Downloadlocatie voor video\'s Locatie om gedownloade video\'s in op te slaan. Voer downloadlocatie in voor video\'s - Speel automatisch via Intent - Speel een video automatisch af indien geopend vanuit een andere app. Standaardresolutie Afspelen met Kodi Kore-app niet gevonden. Kore installeren? - Installeer Kore Toon \"Afspelen met Kodi\"-optie Toont een optie om een video op een Kodi media center af te spelen. Audio @@ -157,143 +153,6 @@ ChannelActivity Instellingen - " - Material is the metaphor. - - - - A material metaphor is the unifying theory of a rationalized space and a system of motion. - The material is grounded in tactile reality, inspired by the study of paper and ink, yet - technologically advanced and open to imagination and magic. - - Surfaces and edges of the material provide visual cues that are grounded in reality. The - use of familiar tactile attributes helps users quickly understand affordances. Yet the - flexibility of the material creates new affordances that supercede those in the physical - world, without breaking the rules of physics. - - The fundamentals of light, surface, and movement are key to conveying how objects move, - interact, and exist in space and in relation to each other. Realistic lighting shows - seams, divides space, and indicates moving parts. - - - - Bold, graphic, intentional. - - - - The foundational elements of print based design typography, grids, space, scale, color, - and use of imagery guide visual treatments. These elements do far more than please the - eye. They create hierarchy, meaning, and focus. Deliberate color choices, edge to edge - imagery, large scale typography, and intentional white space create a bold and graphic - interface that immerse the user in the experience. - - An emphasis on user actions makes core functionality immediately apparent and provides - waypoints for the user. - - - - Motion provides meaning. - - - - Motion respects and reinforces the user as the prime mover. Primary user actions are - inflection points that initiate motion, transforming the whole design. - - All action takes place in a single environment. Objects are presented to the user without - breaking the continuity of experience even as they transform and reorganize. - - Motion is meaningful and appropriate, serving to focus attention and maintain continuity. - Feedback is subtle yet clear. Transitions are efficient yet coherent. - - - - 3D world. - - - - The material environment is a 3D space, which means all objects have x, y, and z - dimensions. The z-axis is perpendicularly aligned to the plane of the display, with the - positive z-axis extending towards the viewer. Every sheet of material occupies a single - position along the z-axis and has a standard 1dp thickness. - - On the web, the z-axis is used for layering and not for perspective. The 3D world is - emulated by manipulating the y-axis. - - - - Light and shadow. - - - - Within the material environment, virtual lights illuminate the scene. Key lights create - directional shadows, while ambient light creates soft shadows from all angles. - - Shadows in the material environment are cast by these two light sources. In Android - development, shadows occur when light sources are blocked by sheets of material at - various positions along the z-axis. On the web, shadows are depicted by manipulating the - y-axis only. The following example shows the card with a height of 6dp. - - - - Resting elevation. - - - - All material objects, regardless of size, have a resting elevation, or default elevation - that does not change. If an object changes elevation, it should return to its resting - elevation as soon as possible. - - - - Component elevations. - - - - The resting elevation for a component type is consistent across apps (e.g., FAB elevation - does not vary from 6dp in one app to 16dp in another app). - - Components may have different resting elevations across platforms, depending on the depth - of the environment (e.g., TV has a greater depth than mobile or desktop). - - - - Responsive elevation and dynamic elevation offsets. - - - - Some component types have responsive elevation, meaning they change elevation in response - to user input (e.g., normal, focused, and pressed) or system events. These elevation - changes are consistently implemented using dynamic elevation offsets. - - Dynamic elevation offsets are the goal elevation that a component moves towards, relative - to the component’s resting state. They ensure that elevation changes are consistent - across actions and component types. For example, all components that lift on press have - the same elevation change relative to their resting elevation. - - Once the input event is completed or cancelled, the component will return to its resting - elevation. - - - - Avoiding elevation interference. - - - - Components with responsive elevations may encounter other components as they move between - their resting elevations and dynamic elevation offsets. Because material cannot pass - through other material, components avoid interfering with one another any number of ways, - whether on a per component basis or using the entire app layout. - - On a component level, components can move or be removed before they cause interference. - For example, a floating action button (FAB) can disappear or move off screen before a - user picks up a card, or it can move if a snackbar appears. - - On the layout level, design your app layout to minimize opportunities for interference. - For example, position the FAB to one side of stream of a cards so the FAB won’t interfere - when a user tries to pick up one of cards. - - - " Zwart reCAPTCHA diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index fcdf32240..84394a32f 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -21,12 +21,9 @@ Descarga de vídeos Local para guardar os vídeos descarregados. Digite o caminho para os vídeos - Reproduzir através de Intent - Reproduzir automaticamente o vídeo se invocado por outra aplicação. Resolução padrão Reproduzir com Kodi Aplicação não encontrada. Instalar o Kore? - Instalar o Kore Mostrar opção \"Reproduzir com Kodi\" Mostra uma opção para reproduzir o vídeo com o Kodi. Áudio @@ -40,8 +37,6 @@ Vídeos similares Idioma preferencial do conteúdo Vídeo e áudio - Informações - Outras Miniatura de vídeos Miniatura de vídeos @@ -158,143 +153,6 @@ Aplicação encerrada O quê:\\nPedido:\\nIdioma do conteúdo:\\nServiço:\\nHora GMT:\\nPacote:\\nVersão:\\nVersão do SO:\\nIP global: Atividade do canal - " - Material is the metaphor. - - - - A material metaphor is the unifying theory of a rationalized space and a system of motion. - The material is grounded in tactile reality, inspired by the study of paper and ink, yet - technologically advanced and open to imagination and magic. - - Surfaces and edges of the material provide visual cues that are grounded in reality. The - use of familiar tactile attributes helps users quickly understand affordances. Yet the - flexibility of the material creates new affordances that supercede those in the physical - world, without breaking the rules of physics. - - The fundamentals of light, surface, and movement are key to conveying how objects move, - interact, and exist in space and in relation to each other. Realistic lighting shows - seams, divides space, and indicates moving parts. - - - - Bold, graphic, intentional. - - - - The foundational elements of print based design typography, grids, space, scale, color, - and use of imagery guide visual treatments. These elements do far more than please the - eye. They create hierarchy, meaning, and focus. Deliberate color choices, edge to edge - imagery, large scale typography, and intentional white space create a bold and graphic - interface that immerse the user in the experience. - - An emphasis on user actions makes core functionality immediately apparent and provides - waypoints for the user. - - - - Motion provides meaning. - - - - Motion respects and reinforces the user as the prime mover. Primary user actions are - inflection points that initiate motion, transforming the whole design. - - All action takes place in a single environment. Objects are presented to the user without - breaking the continuity of experience even as they transform and reorganize. - - Motion is meaningful and appropriate, serving to focus attention and maintain continuity. - Feedback is subtle yet clear. Transitions are efficient yet coherent. - - - - 3D world. - - - - The material environment is a 3D space, which means all objects have x, y, and z - dimensions. The z-axis is perpendicularly aligned to the plane of the display, with the - positive z-axis extending towards the viewer. Every sheet of material occupies a single - position along the z-axis and has a standard 1dp thickness. - - On the web, the z-axis is used for layering and not for perspective. The 3D world is - emulated by manipulating the y-axis. - - - - Light and shadow. - - - - Within the material environment, virtual lights illuminate the scene. Key lights create - directional shadows, while ambient light creates soft shadows from all angles. - - Shadows in the material environment are cast by these two light sources. In Android - development, shadows occur when light sources are blocked by sheets of material at - various positions along the z-axis. On the web, shadows are depicted by manipulating the - y-axis only. The following example shows the card with a height of 6dp. - - - - Resting elevation. - - - - All material objects, regardless of size, have a resting elevation, or default elevation - that does not change. If an object changes elevation, it should return to its resting - elevation as soon as possible. - - - - Component elevations. - - - - The resting elevation for a component type is consistent across apps (e.g., FAB elevation - does not vary from 6dp in one app to 16dp in another app). - - Components may have different resting elevations across platforms, depending on the depth - of the environment (e.g., TV has a greater depth than mobile or desktop). - - - - Responsive elevation and dynamic elevation offsets. - - - - Some component types have responsive elevation, meaning they change elevation in response - to user input (e.g., normal, focused, and pressed) or system events. These elevation - changes are consistently implemented using dynamic elevation offsets. - - Dynamic elevation offsets are the goal elevation that a component moves towards, relative - to the component’s resting state. They ensure that elevation changes are consistent - across actions and component types. For example, all components that lift on press have - the same elevation change relative to their resting elevation. - - Once the input event is completed or cancelled, the component will return to its resting - elevation. - - - - Avoiding elevation interference. - - - - Components with responsive elevations may encounter other components as they move between - their resting elevations and dynamic elevation offsets. Because material cannot pass - through other material, components avoid interfering with one another any number of ways, - whether on a per component basis or using the entire app layout. - - On a component level, components can move or be removed before they cause interference. - For example, a floating action button (FAB) can disappear or move off screen before a - user picks up a card, or it can move if a snackbar appears. - - On the layout level, design your app layout to minimize opportunities for interference. - For example, position the FAB to one side of stream of a cards so the FAB won’t interfere - when a user tries to pick up one of cards. - - - " Abrir no modo “popup“ Preto diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 3a6a64f86..6c28f910b 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -28,8 +28,6 @@ Locul în care se vor stoca fișierele audio descărcate Introduceți calea de descărcare pentru fișierele audio. - Redare automată intenționată - Redați automat un videoclip atunci când este chemat din altă aplicație. Rezoluție implicită Redați folosind Kodi Aplicația Kore nu a fost găsită. Instalați Kore? diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 288b1137a..dcb8221a7 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -16,16 +16,12 @@ Выбрать браузер поворот Настройки - Использовать внешний проигрыватель Место для загрузок Папка для хранения загруженных видео Введите путь к папке для загрузки видео - Автопроигрывание через интернет - Автоматически воспроизводить видео, открытое через другое приложение Разрешение по-умолчанию Воспроизвести с помощью Kodi Приложение Kore не наидено. Установить Kore? - Установить Kore Показывать опцию \"Воспроизвести с помощью Kodi\" Показать опцию воспроизведения видео через Kodi media center Аудио diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index ec90d8727..0ace30a7e 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -29,8 +29,6 @@ Vložte cestu kam sa budú ukladať zvukové súbory. Cesta kam sa bude ukladať prevzaté audio - Automatické prehrávanie na vyžiadanie - Automatický prehrá video na žiadosť z inej aplikácie. Štandardné rozlíšenie Prehrať cez Kodi Aplikácia Kore nenajdená. Inštalovať Kore? @@ -155,143 +153,6 @@ Aktivita kanálov Nastavenia - " - Materiál je metaforou. - - - - Materiál metafora je zjednocujúci teórie racionálne priestoru a systém pohybu. - Materiál je zakotvená v taktilné skutočnosti, inšpirované štúdiom papiera a atramentu, ale napriek tomu - technologicky pokročilé a otvorené fantáziu a mágiu. - - Povrchy a hrany materiálu poskytujú vizuálnu pokyny, ktoré sú zakotvené v realite. - Použitie známych hmatových vlastností, pomáha používateľom rýchlo pochopiť affordances. napriek tomu - Pružnosť materiálu vytvára nové affordances že nadradené tie fyzické - svet bez toho, aby porušenie pravidiel fyziky. - - Základy svetla, povrchu a pohyb sú kľúčom k dopravovanie, ako sa objekty pohybovať, - komunikovať, a existujú v priestore a vo vzťahu k sebe navzájom. Realistické svetelné šou - švy, rozdeľuje priestor, a indikuje pohyblivých častí. - - - - Bold, kreslený, úmyselné. - - - - Foundational prvky tlače na dizajn typografie, rošty, priestor, mierka, farby, - a používanie metafor riadiť vizuálne procedúry. Tieto prvky robiť oveľa viac, než potešiť - oko. Vytvárajú hierarchie, zmysel a zameranie. výber farieb úmyselné, od okraja k okraju - snímok, rozsiahle typografie a úmyselné biely priestor vytvoriť tučný a grafický - rozhranie, ktoré ponorí užívateľov v zážitku. - - Dôraz na užívateľských akcií činí základné funkcie okamžite zrejmé a poskytuje - waypointy pre užívateľov. - - - - Motion poskytuje význam. - - - - Návrh rešpektuje a posilňuje užívateľa ako hnacia sila. Primárne akcie užívateľa sú - inflexné body, ktoré iniciujú pohyb, transformáciu celej konštrukcie. - - Celá akcia prebieha v jedinom prostredí. Objekty sú prezentované užívateľovi bez toho, - prelomenie kontinuity skúseností, aj keď ich transformáciu a reorganizáciu. - - Pohyb je zmysluplné a vhodné, slúžiace sústrediť pozornosť a zachovanie kontinuity. - Spätná väzba je jemné, ale napriek tomu jasné. Prechody sú ef fi točné ešte koherentné. - - - - 3D svet. - - - - Materiál prostredie je 3D priestore, čo znamená, že všetky objekty majú X, Y, a Z - rozmery. Os je kolmo zarovnaný k rovine zobrazenie, s - kladná os rozširuje smerom k divákovi. Každý list materiálu, umiestnené v jednom - poloha pozdĺž osi a má štandardnú hrúbku 1DP. - - Na webe, os sa používa pre vrstvenie a nie pre perspektívy. 3D svet - emuloval tým, že manipuluje os y. - - - - Svetlo a tieň. - - - - V hmotnom prostredí, virtuálne svetlá osvetľujú scénu. Kľúčové svetla vytvárajú - smerové tiene, zatiaľ čo okolité svetlo vytvára mäkké tiene zo všetkých strán. - - Tiene v hmotnom prostredí sú obsadené týmito dvoma svetelnými zdrojmi. v Android - vývoj, dochádza tiene, keď sú svetelné zdroje blokované plechového materiálu, na - Rôzne pozície pozdĺž osi. Na webe tiene sú znázornené manipuláciou - Iba os y. Nasledujúci príklad ukazuje kartu s výškou 6dp. - - - - Odpočíva nadmorskú výšku. - - - - Všetky významné objekty, bez ohľadu na ich veľkosť, majú výšku odpočíva, alebo predvolené výšku - to nič nemení. V prípade, že objekt sa mení výšku, mal by sa vrátiť do svojej pokojovej - nadmorskej výšky čo najskôr. - - - - Component výškach. - - - - Pokojová nadmorská výška pre typ komponentu je konzistentné naprieč aplikáciami (napr FAB elevácie - nelíši od 6dp v jednej aplikácii na 16dp v inej aplikácii). - - Zložky môžu mať rôzne pokojovej výšky medzi platformami, v závislosti od hĺbky - životného prostredia (napr televízor má väčšiu hĺbku, ako mobilný telefón alebo plochy). - - - - Citlivé nadmorská výška a dynamické elevácie offsety. - - - - Niektoré typy komponentov majú citlivejší výšku, čo znamená, že meniť výšku v odozve - na vstup používateľa (napríklad normálny priebeh, sa zameral, a lisované), alebo systémové udalosti. tieto elevácie - Zmeny sa vykonávali dôsledne používať dynamické výškové posuny. - - Dynamické výškové posuny sú cieľom nadmorská výška, ktorá zložka sa pohybuje smerom, relatívna - do pokojového stavu súčasti. Zaisťujú, že zmeny elevácie sú v súlade - naprieč akcií a typov komponentov. Napríklad všetky súčasti, ktoré vlek na lise majú - rovnaká zmena prevýšenie vo vzťahu k ich prevýšenie odpočinku. - - Akonáhle je vstupná udalosť dokončenie alebo zrušená, bude zložka vráti do svojej pokojovej - nadmorská výška. - - - - Vyhnúť sa rušenie elevácie. - - - - Zložky s citlivými nadmorských výškach môže stretnúť s ďalšími komponentmi, ako sa pohybovať medzi - ich odpočinku vyvýšeniny a dynamické výškové odsadenie. Vzhľadom k tomu, materiál nemôže prejsť - prostredníctvom iného materiálu, komponenty nenarušovať spolu navzájom nejakom množstvo ciest, - či už na úrovni jednotlivých komponentov alebo s použitím kompletné rozloženie aplikácie. - - Na úrovni komponentov môžu zložky presunúť alebo odstrániť skôr, než spôsobia rušenie. - Napríklad tlačidlo plávajúce akcie (FAB) môžu zmiznúť alebo presunúť mimo obrazovku pred - Užívateľ zdvihne kartu, alebo sa môže pohybovať, ak sa objaví snackbar. - - Na úrovni Usporiadanie, riešenie rozvrhnutie aplikácie, aby sa minimalizovalo príležitostí k rušeniu. - Napríklad, umiestnenie FAB na jednej strane prúdu niekoľkých kariet, takže FAB nebude zasahovať - ak sa užívateľ pokúsi vyzdvihnúť jednu z kariet. - - - " reCAPTCHA Výzva reCAPTCHA diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 6ee3e2f9a..653575e6e 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -24,7 +24,6 @@ Privzeta ločljivost Predvajaj s Kodi Programa Kore ni mogoče najti. Ali želite program namestiti? - Namesti program Kore Pokaži možnost \"Predvajaj s Kodi\" Privzet zapis zvoka Zvok @@ -40,10 +39,7 @@ Sličica predogleda videa Sličica predogleda videa Sličica pošiljalnika - Samodejno predvajanje prek vmesnika Intent - Začne samodejno predvajanje videa, ko je zagnan prek drugega programa. Pokaže možnost predvajanja videa preko predstavnega središča Kodi. - Drugo Všeč mi je Ni mi všeč diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index bc8098a95..933733ba4 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -16,16 +16,12 @@ Отвори помоћу ротација Поставке - Користи спољашњи плејер Одредиште преузимања за видео Путања за упис преузетих видеа. Унесите путању за преузимање видеа - Аутопуштање преко интента - Аутоматско пуштање видеа по позиву из друге апликације. Подразумевана резолуција Пусти помоћу Кодија Апликација Кор (Kore) није нађена. Инсталирати Кор? - Инсталирај Кор Прикажи „Пусти помоћу Кодија“ Приказ опције за пуштање видеа у Коди медија центру. Аудио @@ -164,143 +160,6 @@ Активност канала Поставке - " - Material is the metaphor. - - - - A material metaphor is the unifying theory of a rationalized space and a system of motion. - The material is grounded in tactile reality, inspired by the study of paper and ink, yet - technologically advanced and open to imagination and magic. - - Surfaces and edges of the material provide visual cues that are grounded in reality. The - use of familiar tactile attributes helps users quickly understand affordances. Yet the - flexibility of the material creates new affordances that supercede those in the physical - world, without breaking the rules of physics. - - The fundamentals of light, surface, and movement are key to conveying how objects move, - interact, and exist in space and in relation to each other. Realistic lighting shows - seams, divides space, and indicates moving parts. - - - - Bold, graphic, intentional. - - - - The foundational elements of print based design typography, grids, space, scale, color, - and use of imagery guide visual treatments. These elements do far more than please the - eye. They create hierarchy, meaning, and focus. Deliberate color choices, edge to edge - imagery, large scale typography, and intentional white space create a bold and graphic - interface that immerse the user in the experience. - - An emphasis on user actions makes core functionality immediately apparent and provides - waypoints for the user. - - - - Motion provides meaning. - - - - Motion respects and reinforces the user as the prime mover. Primary user actions are - inflection points that initiate motion, transforming the whole design. - - All action takes place in a single environment. Objects are presented to the user without - breaking the continuity of experience even as they transform and reorganize. - - Motion is meaningful and appropriate, serving to focus attention and maintain continuity. - Feedback is subtle yet clear. Transitions are efficient yet coherent. - - - - 3D world. - - - - The material environment is a 3D space, which means all objects have x, y, and z - dimensions. The z-axis is perpendicularly aligned to the plane of the display, with the - positive z-axis extending towards the viewer. Every sheet of material occupies a single - position along the z-axis and has a standard 1dp thickness. - - On the web, the z-axis is used for layering and not for perspective. The 3D world is - emulated by manipulating the y-axis. - - - - Light and shadow. - - - - Within the material environment, virtual lights illuminate the scene. Key lights create - directional shadows, while ambient light creates soft shadows from all angles. - - Shadows in the material environment are cast by these two light sources. In Android - development, shadows occur when light sources are blocked by sheets of material at - various positions along the z-axis. On the web, shadows are depicted by manipulating the - y-axis only. The following example shows the card with a height of 6dp. - - - - Resting elevation. - - - - All material objects, regardless of size, have a resting elevation, or default elevation - that does not change. If an object changes elevation, it should return to its resting - elevation as soon as possible. - - - - Component elevations. - - - - The resting elevation for a component type is consistent across apps (e.g., FAB elevation - does not vary from 6dp in one app to 16dp in another app). - - Components may have different resting elevations across platforms, depending on the depth - of the environment (e.g., TV has a greater depth than mobile or desktop). - - - - Responsive elevation and dynamic elevation offsets. - - - - Some component types have responsive elevation, meaning they change elevation in response - to user input (e.g., normal, focused, and pressed) or system events. These elevation - changes are consistently implemented using dynamic elevation offsets. - - Dynamic elevation offsets are the goal elevation that a component moves towards, relative - to the component’s resting state. They ensure that elevation changes are consistent - across actions and component types. For example, all components that lift on press have - the same elevation change relative to their resting elevation. - - Once the input event is completed or cancelled, the component will return to its resting - elevation. - - - - Avoiding elevation interference. - - - - Components with responsive elevations may encounter other components as they move between - their resting elevations and dynamic elevation offsets. Because material cannot pass - through other material, components avoid interfering with one another any number of ways, - whether on a per component basis or using the entire app layout. - - On a component level, components can move or be removed before they cause interference. - For example, a floating action button (FAB) can disappear or move off screen before a - user picks up a card, or it can move if a snackbar appears. - - On the layout level, design your app layout to minimize opportunities for interference. - For example, position the FAB to one side of stream of a cards so the FAB won’t interfere - when a user tries to pick up one of cards. - - - " Стопка reCAPTCHA стопка diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index a0e17bb4a..83fd8a182 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -143,7 +143,6 @@ Lütfen bir kullanılabilir indirme dizini seçin. - Ayarlar İndirme menüsü kurulamıyor. diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 70ce224d2..56429e262 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -23,8 +23,6 @@ 视频下载路径 存放已下载视频的路径。 输入视频的下载路径 - 刻意自动播放 - 当另一个程式发出要求时自动播放视频。 默认分辨率 用 Kodi 播放 找不到 Kore 应用程式,您要安装 Kore 吗? diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 225a7014c..2ff8465b2 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -18,8 +18,6 @@ 影片下載路徑 存放已下載影片的路徑。 輸入影片的下載路徑 - 刻意自動播放 - 當另一個程式發出要求時自動播放影片。 預設解析度 用 Kodi 播放 找不到 Kore 應用程式,您要安裝 Kore 嗎? diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 12049e0a2..c7cb3a9c4 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -22,8 +22,6 @@ 影片下載路徑 存放已下載影片的路徑。 輸入影片的下載路徑 - 刻意自動播放 - 當另一個程式發出要求時自動播放影片。 預設解析度 用 Kodi 播放 顯示以 Kodi 媒體中心播放影片的選項。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f91c51f3f..13ca6c2bc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -197,95 +197,6 @@ MD5 SHA1 ChannelActivity - - "Material is the metaphor.\n\n" - - "A material metaphor is the unifying theory of a rationalized space and a system of motion." - "The material is grounded in tactile reality, inspired by the study of paper and ink, yet " - "technologically advanced and open to imagination and magic.\n" - "Surfaces and edges of the material provide visual cues that are grounded in reality. The " - "use of familiar tactile attributes helps users quickly understand affordances. Yet the " - "flexibility of the material creates new affordances that supercede those in the physical " - "world, without breaking the rules of physics.\n" - "The fundamentals of light, surface, and movement are key to conveying how objects move, " - "interact, and exist in space and in relation to each other. Realistic lighting shows " - "seams, divides space, and indicates moving parts.\n\n" - - "Bold, graphic, intentional.\n\n" - - "The foundational elements of print based design typography, grids, space, scale, color, " - "and use of imagery guide visual treatments. These elements do far more than please the " - "eye. They create hierarchy, meaning, and focus. Deliberate color choices, edge to edge " - "imagery, large scale typography, and intentional white space create a bold and graphic " - "interface that immerse the user in the experience.\n" - "An emphasis on user actions makes core functionality immediately apparent and provides " - "waypoints for the user.\n\n" - - "Motion provides meaning.\n\n" - - "Motion respects and reinforces the user as the prime mover. Primary user actions are " - "inflection points that initiate motion, transforming the whole design.\n" - "All action takes place in a single environment. Objects are presented to the user without " - "breaking the continuity of experience even as they transform and reorganize.\n" - "Motion is meaningful and appropriate, serving to focus attention and maintain continuity. " - "Feedback is subtle yet clear. Transitions are efficient yet coherent.\n\n" - - "3D world.\n\n" - - "The material environment is a 3D space, which means all objects have x, y, and z " - "dimensions. The z-axis is perpendicularly aligned to the plane of the display, with the " - "positive z-axis extending towards the viewer. Every sheet of material occupies a single " - "position along the z-axis and has a standard 1dp thickness.\n" - "On the web, the z-axis is used for layering and not for perspective. The 3D world is " - "emulated by manipulating the y-axis.\n\n" - - "Light and shadow.\n\n" - - "Within the material environment, virtual lights illuminate the scene. Key lights create " - "directional shadows, while ambient light creates soft shadows from all angles.\n" - "Shadows in the material environment are cast by these two light sources. In Android " - "development, shadows occur when light sources are blocked by sheets of material at " - "various positions along the z-axis. On the web, shadows are depicted by manipulating the " - "y-axis only. The following example shows the card with a height of 6dp.\n\n" - - "Resting elevation.\n\n" - - "All material objects, regardless of size, have a resting elevation, or default elevation " - "that does not change. If an object changes elevation, it should return to its resting " - "elevation as soon as possible.\n\n" - - "Component elevations.\n\n" - - "The resting elevation for a component type is consistent across apps (e.g., FAB elevation " - "does not vary from 6dp in one app to 16dp in another app).\n" - "Components may have different resting elevations across platforms, depending on the depth " - "of the environment (e.g., TV has a greater depth than mobile or desktop).\n\n" - - "Responsive elevation and dynamic elevation offsets.\n\n" - - "Some component types have responsive elevation, meaning they change elevation in response " - "to user input (e.g., normal, focused, and pressed) or system events. These elevation " - "changes are consistently implemented using dynamic elevation offsets.\n" - "Dynamic elevation offsets are the goal elevation that a component moves towards, relative " - "to the component’s resting state. They ensure that elevation changes are consistent " - "across actions and component types. For example, all components that lift on press have " - "the same elevation change relative to their resting elevation.\n" - "Once the input event is completed or cancelled, the component will return to its resting " - "elevation.\n\n" - - "Avoiding elevation interference.\n\n" - - "Components with responsive elevations may encounter other components as they move between " - "their resting elevations and dynamic elevation offsets. Because material cannot pass " - "through other material, components avoid interfering with one another any number of ways, " - "whether on a per component basis or using the entire app layout.\n" - "On a component level, components can move or be removed before they cause interference. " - "For example, a floating action button (FAB) can disappear or move off screen before a " - "user picks up a card, or it can move if a snackbar appears.\n" - "On the layout level, design your app layout to minimize opportunities for interference. " - "For example, position the FAB to one side of stream of a cards so the FAB won’t interfere " - "when a user tries to pick up one of cards.\n\n" - Settings reCAPTCHA reCAPTCHA Challenge diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 9cf8160c3..06d5eb0e9 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -7,13 +7,6 @@ @android:color/black - - -