From a37d8f083a336ae43d9d7e11d94b7e2eaca05461 Mon Sep 17 00:00:00 2001 From: Mauricio Colli Date: Thu, 9 Mar 2017 04:42:40 -0300 Subject: [PATCH] Implement popup mode - Add icons replay, fast_forward - Add strings - Add menu entry - Add as option to open link directly to popup mode --- app/src/main/AndroidManifest.xml | 61 ++ .../org/schabi/newpipe/PopupActivity.java | 137 +++ .../newpipe/detail/ActionBarHandler.java | 11 + .../detail/VideoItemDetailFragment.java | 54 +- .../newpipe/player/ExoPlayerActivity.java | 51 +- .../newpipe/player/PopupVideoPlayer.java | 826 ++++++++++++++++++ .../newpipe/player/popup/PopupViewHolder.java | 93 ++ .../newpipe/player/popup/StateInterface.java | 17 + .../schabi/newpipe/util/PermissionHelper.java | 29 +- .../ic_action_av_fast_forward.png | Bin 0 -> 575 bytes .../res/drawable-hdpi/ic_replay_white.png | Bin 0 -> 675 bytes .../ic_action_av_fast_forward.png | Bin 0 -> 277 bytes .../res/drawable-mdpi/ic_replay_white.png | Bin 0 -> 457 bytes .../ic_action_av_fast_forward.png | Bin 0 -> 574 bytes .../res/drawable-xhdpi/ic_replay_white.png | Bin 0 -> 908 bytes .../ic_action_av_fast_forward.png | Bin 0 -> 889 bytes .../res/drawable-xxhdpi/ic_replay_white.png | Bin 0 -> 1390 bytes .../ic_action_av_fast_forward.png | Bin 0 -> 1381 bytes .../res/drawable-xxxhdpi/ic_replay_white.png | Bin 0 -> 1885 bytes .../main/res/drawable/popup_controls_bg.xml | 9 + app/src/main/res/layout/player_popup.xml | 110 +++ .../res/layout/player_popup_notification.xml | 69 ++ app/src/main/res/menu/videoitem_detail.xml | 4 + app/src/main/res/values/strings.xml | 2 + 24 files changed, 1422 insertions(+), 51 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/PopupActivity.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/popup/PopupViewHolder.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/popup/StateInterface.java create mode 100644 app/src/main/res/drawable-hdpi/ic_action_av_fast_forward.png create mode 100644 app/src/main/res/drawable-hdpi/ic_replay_white.png create mode 100644 app/src/main/res/drawable-mdpi/ic_action_av_fast_forward.png create mode 100644 app/src/main/res/drawable-mdpi/ic_replay_white.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_action_av_fast_forward.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_replay_white.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_av_fast_forward.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_replay_white.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_action_av_fast_forward.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_replay_white.png create mode 100644 app/src/main/res/drawable/popup_controls_bg.xml create mode 100644 app/src/main/res/layout/player_popup.xml create mode 100644 app/src/main/res/layout/player_popup_notification.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bc050f2ce..4e8ca0676 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/PopupActivity.java b/app/src/main/java/org/schabi/newpipe/PopupActivity.java new file mode 100644 index 000000000..59763070b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/PopupActivity.java @@ -0,0 +1,137 @@ +package org.schabi.newpipe; + +import android.app.Activity; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import android.widget.Toast; + +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.player.PopupVideoPlayer; +import org.schabi.newpipe.util.NavStack; +import org.schabi.newpipe.util.PermissionHelper; + +import java.util.Collection; +import java.util.HashSet; + +/** + * This Acitivty is designed to route share/open intents to the specified service, and + * to the part of the service which can handle the url. + */ + +public class PopupActivity extends Activity { + private static final String TAG = RouterActivity.class.toString(); + + /** + * Removes invisible separators (\p{Z}) and punctuation characters including + * brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for + * more details. + */ + private final static String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + handleIntent(getIntent()); + finish(); + } + + + private static String removeHeadingGibberish(final String input) { + int start = 0; + for (int i = input.indexOf("://") - 1; i >= 0; i--) { + if (!input.substring(i, i + 1).matches("\\p{L}")) { + start = i + 1; + break; + } + } + return input.substring(start, input.length()); + } + + private static String trim(final String input) { + if (input == null || input.length() < 1) { + return input; + } else { + String output = input; + while (output.length() > 0 && output.substring(0, 1).matches(REGEX_REMOVE_FROM_URL)) { + output = output.substring(1); + } + while (output.length() > 0 + && output.substring(output.length() - 1, output.length()).matches(REGEX_REMOVE_FROM_URL)) { + output = output.substring(0, output.length() - 1); + } + return output; + } + } + + /** + * Retrieves all Strings which look remotely like URLs from a text. + * Used if NewPipe was called through share menu. + * + * @param sharedText text to scan for URLs. + * @return potential URLs + */ + private String[] getUris(final String sharedText) { + final Collection result = new HashSet<>(); + if (sharedText != null) { + final String[] array = sharedText.split("\\p{Space}"); + for (String s : array) { + s = trim(s); + if (s.length() != 0) { + if (s.matches(".+://.+")) { + result.add(removeHeadingGibberish(s)); + } else if (s.matches(".+\\..+")) { + result.add("http://" + s); + } + } + } + } + return result.toArray(new String[result.size()]); + } + + private void handleIntent(Intent intent) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && !PermissionHelper.checkSystemAlertWindowPermission(this)) { + Toast.makeText(this, R.string.msg_popup_permission, Toast.LENGTH_LONG).show(); + return; + } + String videoUrl = ""; + StreamingService service = null; + + // first gather data and find service + if (intent.getData() != null) { + // this means the video was called though another app + videoUrl = intent.getData().toString(); + } else if (intent.getStringExtra(Intent.EXTRA_TEXT) != null) { + //this means that vidoe was called through share menu + String extraText = intent.getStringExtra(Intent.EXTRA_TEXT); + videoUrl = getUris(extraText)[0]; + } + + service = NewPipe.getServiceByUrl(videoUrl); + if (service == null) { + Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG) + .show(); + return; + } else { + Intent callIntent = new Intent(); + switch (service.getLinkTypeByUrl(videoUrl)) { + case STREAM: + callIntent.setClass(this, PopupVideoPlayer.class); + break; + case PLAYLIST: + Log.e(TAG, "NOT YET DEFINED"); + break; + default: + Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show(); + return; + } + + callIntent.putExtra(NavStack.URL, videoUrl); + callIntent.putExtra(NavStack.SERVICE_ID, service.getServiceId()); + startService(callIntent); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/detail/ActionBarHandler.java b/app/src/main/java/org/schabi/newpipe/detail/ActionBarHandler.java index 30ddf3da9..eccfc0919 100644 --- a/app/src/main/java/org/schabi/newpipe/detail/ActionBarHandler.java +++ b/app/src/main/java/org/schabi/newpipe/detail/ActionBarHandler.java @@ -53,6 +53,7 @@ class ActionBarHandler { // those are edited directly. Typically VideoItemDetailFragment will implement those callbacks. private OnActionListener onShareListener; private OnActionListener onOpenInBrowserListener; + private OnActionListener onOpenInPopupListener; private OnActionListener onDownloadListener; private OnActionListener onPlayWithKodiListener; private OnActionListener onPlayAudioListener; @@ -190,6 +191,12 @@ class ActionBarHandler { activity.startActivity(intent); return true; } + case R.id.menu_item_popup: { + if(onOpenInPopupListener != null) { + onOpenInPopupListener.onActionSelected(selectedVideoStream); + } + return true; + } default: Log.e(TAG, "Menu Item not known"); } @@ -208,6 +215,10 @@ class ActionBarHandler { onOpenInBrowserListener = listener; } + public void setOnOpenInPopupListener(OnActionListener listener) { + onOpenInPopupListener = listener; + } + public void setOnDownloadListener(OnActionListener listener) { onDownloadListener = listener; } 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 8dc403f11..a221e4e94 100644 --- a/app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailFragment.java @@ -33,8 +33,6 @@ import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; - -import com.google.android.exoplayer.util.Util; import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.assist.FailReason; @@ -56,12 +54,13 @@ import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.player.BackgroundPlayer; import org.schabi.newpipe.player.ExoPlayerActivity; import org.schabi.newpipe.player.PlayVideoActivity; +import org.schabi.newpipe.player.PopupVideoPlayer; import org.schabi.newpipe.report.ErrorActivity; -import java.util.Vector; - import org.schabi.newpipe.util.NavStack; import org.schabi.newpipe.util.PermissionHelper; +import java.util.Vector; + import static android.app.Activity.RESULT_OK; import static org.schabi.newpipe.ReCaptchaActivity.RECAPTCHA_REQUEST; @@ -324,6 +323,19 @@ public class VideoItemDetailFragment extends Fragment { @Override public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { streamThumbnail = loadedImage; + + if (streamThumbnail != null) { + // TODO: Change the thumbnail implementation + + // When the thumbnail is not loaded yet, it not passes to the service in time + // 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); + } } @Override @@ -365,6 +377,28 @@ public class VideoItemDetailFragment extends Fragment { } }); + actionBarHandler.setOnOpenInPopupListener(new ActionBarHandler.OnActionListener() { + @Override + public void onActionSelected(int selectedStreamId) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && !PermissionHelper.checkSystemAlertWindowPermission(activity)) { + Toast.makeText(activity, R.string.msg_popup_permission, Toast.LENGTH_LONG).show(); + return; + } + 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); + activity.startService(i); + } + }); + actionBarHandler.setOnPlayWithKodiListener(new ActionBarHandler.OnActionListener() { @Override public void onActionSelected(int selectedStreamId) { @@ -753,13 +787,16 @@ public class VideoItemDetailFragment extends Fragment { 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); + .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())) { @@ -770,7 +807,10 @@ public class VideoItemDetailFragment extends Fragment { Intent intent = new Intent(activity, ExoPlayerActivity.class) .setDataAndType(Uri.parse(selectedVideoStream.url), MediaFormat.getMimeById(selectedVideoStream.format)) - .putExtra(ExoPlayerActivity.CONTENT_TYPE_EXTRA, Util.TYPE_OTHER); + + .putExtra(ExoPlayerActivity.VIDEO_TITLE, info.title) + .putExtra(ExoPlayerActivity.CHANNEL_NAME, info.uploader); + //.putExtra(ExoPlayerActivity.CONTENT_TYPE_EXTRA, Util.TYPE_OTHER); activity.startActivity(intent); // HERE !!! } 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 7fe97d0dc..c868bb722 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java @@ -1,39 +1,3 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Extended by Christian Schabesberger on 24.12.15. - *

- * Copyright (C) Christian Schabesberger 2015 - * ExoPlayerActivity.java is part of NewPipe. all changes are under GPL3 - *

- * NewPipe is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - *

- * NewPipe is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - *

- * You should have received a copy of the GNU General Public License - * along with NewPipe. If not, see . - */ - package org.schabi.newpipe.player; import android.app.Activity; @@ -56,6 +20,7 @@ import org.schabi.newpipe.R; 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; @@ -94,13 +59,13 @@ public class ExoPlayerActivity extends Activity implements OnPreparedListener, O videoControls.setVisibilityListener(new VideoControlsVisibilityListener() { @Override public void onControlsShown() { - Log.d(TAG, "------------ onControlsShown() called"); + if (DEBUG) Log.d(TAG, "------------ onControlsShown() called"); showSystemUi(); } @Override public void onControlsHidden() { - Log.d(TAG, "------------ onControlsHidden() called"); + if (DEBUG) Log.d(TAG, "------------ onControlsHidden() called"); hideSystemUi(); } }); @@ -109,13 +74,13 @@ public class ExoPlayerActivity extends Activity implements OnPreparedListener, O @Override public void onPrepared() { - Log.d(TAG, "onPrepared() called"); + if (DEBUG) Log.d(TAG, "onPrepared() called"); videoView.start(); } @Override public void onCompletion() { - Log.d(TAG, "onCompletion() called"); + if (DEBUG) Log.d(TAG, "onCompletion() called"); // videoView.getVideoControls().setButtonListener(); //videoView.restart(); videoControls.setRewindButtonRemoved(true); @@ -144,13 +109,13 @@ public class ExoPlayerActivity extends Activity implements OnPreparedListener, O } private void showSystemUi() { - Log.d(TAG, "showSystemUi() called"); + if (DEBUG) Log.d(TAG, "showSystemUi() called"); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); getWindow().getDecorView().setSystemUiVisibility(0); } private void hideSystemUi() { - Log.d(TAG, "hideSystemUi() called"); + 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 @@ -234,7 +199,7 @@ public class ExoPlayerActivity extends Activity implements OnPreparedListener, O protected void onPlayPauseClick() { super.onPlayPauseClick(); if (videoView == null) return; - Log.d(TAG, "onPlayPauseClick() called" + videoView.getDuration()+" position= "+ videoView.getCurrentPosition()); + if (DEBUG) Log.d(TAG, "onPlayPauseClick() called" + videoView.getDuration() + " position= " + videoView.getCurrentPosition()); if (isFinished) { videoView.restart(); setRewindButtonRemoved(false); diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java new file mode 100644 index 000000000..7d211ecdb --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -0,0 +1,826 @@ +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; +import android.app.Service; +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.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.net.Uri; +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; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.WindowManager; +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; + +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.extractor.MediaFormat; +import org.schabi.newpipe.extractor.NewPipe; +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; + +public class PopupVideoPlayer extends Service implements StateInterface { + private static final String TAG = ".PopupVideoPlayer"; + private static final boolean DEBUG = false; + private static int CURRENT_STATE = -1; + + 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 + + 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 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 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 Repeater progressPollRepeater = new Repeater(); + private SharedPreferences sharedPreferences; + + @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(); + } + + 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 + "]"); + switch (intent.getAction()) { + case InternalListener.ACTION_CLOSE: + internalListener.onVideoClose(); + break; + case InternalListener.ACTION_PLAY_PAUSE: + internalListener.onVideoPlayPause(); + break; + case InternalListener.ACTION_OPEN_DETAIL: + internalListener.onOpenDetail(PopupVideoPlayer.this, videoUrl); + break; + case InternalListener.ACTION_UPDATE_THUMB: + internalListener.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); + registerReceiver(broadcastReceiver, intentFilter); + } + + @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); + + 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(); + + 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()); + } + + private NotificationCompat.Builder createNotification() { + notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_popup_notification); + if (videoThumbnail != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, videoThumbnail); + else notRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail); + notRemoteView.setOnClickPendingIntent(R.id.notificationPlayPause, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(InternalListener.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); + notRemoteView.setOnClickPendingIntent(R.id.notificationContent, + PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(InternalListener.ACTION_OPEN_DETAIL), PendingIntent.FLAG_UPDATE_CURRENT)); + + return new NotificationCompat.Builder(this) + .setOngoing(true) + .setSmallIcon(R.drawable.ic_play_arrow_white_48dp) + .setContent(notRemoteView); + } + + /** + * Updates the notification, and the play/pause button in it. + * Used for changes on the remoteView + * + * @param drawableId if != -1, sets the drawable with that id on the play/pause button + */ + private void updateNotification(int drawableId) { + if (DEBUG) Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]"); + if (notBuilder == null || notRemoteView == null) return; + if (drawableId != -1) notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); + 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; + } + + 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(); + } + + /** + * 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(); + } + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + updateScreenSize(); + } + + @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); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + /////////////////////////////////////////////////////////////////////////// + // 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; + } + } + + @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())); + + changeState(STATE_PLAYING); + progressPollRepeater.start(); + emVideoView.start(); + + } + + 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))); + } + if (DEBUG && bufferPercent % 10 == 0) { //Limit log + Log.d(TAG, "updateProgress() called with: isVisible = " + viewHolder.isControlsVisible() + ", currentProgress = [" + currentProgress + "], duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]"); + } + } + + 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"); + 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); + } + } + + 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 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);*/ + } + + @Override + public void onCompletion() { + if (DEBUG) Log.d(TAG, "onCompletion() called"); + changeState(STATE_COMPLETED); + progressPollRepeater.stop(); + } + + @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)); + } + + @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); + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + if (DEBUG) Log.d(TAG, "onProgressChanged() called with: seekBar = [" + seekBar + "], progress = [" + seekBar.getProgress() + "]"); + emVideoView.seekTo(seekBar.getProgress()); + + } + + /////////////////////////////////////////////////////////////////////////// + // 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 { + private int initialPopupX, initialPopupY; + 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 (!emVideoView.isPlaying()) return false; + if (e.getX() > popupWidth / 2) internalListener.onFastForward(); + else internalListener.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(); + 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(); + 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) { + isMoving = true; + float diffX = (int) (e2.getRawX() - e1.getRawX()), posX = (int) (initialPopupX + diffX); + float diffY = (int) (e2.getRawY() - e1.getRawY()), posY = (int) (initialPopupY + diffY); + + if (posX > (screenWidth - popupWidth)) posX = (int) (screenWidth - popupWidth); + else if (posX < 0) posX = 0; + + if (posY > (screenHeight - popupHeight)) posY = (int) (screenHeight - popupHeight); + else if (posY < 0) posY = 0; + + windowLayoutParams.x = (int) posX; + windowLayoutParams.y = (int) posY; + + if (DEBUG) 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); + 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); + } + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + gestureDetector.onTouchEvent(event); + if (event.getAction() == MotionEvent.ACTION_UP && isMoving) { + isMoving = false; + onScrollEnd(); + } + return true; + } + + } + + /** + * Fetcher used if open by a link out of NewPipe + */ + private class FetcherRunnable implements Runnable { + private final Intent intent; + private final Handler mainHandler; + private final boolean printStreams = true; + + + FetcherRunnable(Intent intent) { + this.intent = intent; + this.mainHandler = new Handler(PopupVideoPlayer.this.getMainLooper()); + } + + @Override + public void run() { + StreamExtractor streamExtractor; + try { + StreamingService service = NewPipe.getService(0); + if (service == null) return; + streamExtractor = service.getExtractorInstance(intent.getStringExtra(NavStack.URL)); + StreamInfo info = StreamInfo.getVideoInfo(streamExtractor); + String defaultResolution = sharedPreferences.getString( + getResources().getString(R.string.default_resolution_key), + getResources().getString(R.string.default_resolution_value)); + + String chosen = "", secondary = "", fallback = ""; + for (VideoStream item : info.video_streams) { + if (DEBUG && printStreams) { + Log.d(TAG, "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; + + } + } + + 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 + + "\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; + mainHandler.post(new Runnable() { + @Override + public void run() { + playVideo(streamUri); + } + }); + imageLoader.loadImage(info.thumbnail_url, displayImageOptions, new SimpleImageLoadingListener() { + @Override + public void onLoadingComplete(String imageUri, View view, final Bitmap loadedImage) { + mainHandler.post(new Runnable() { + @Override + public void run() { + videoThumbnail = loadedImage; + if (videoThumbnail != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, videoThumbnail); + updateNotification(-1); + } + }); + } + }); + } catch (Exception e) { + e.printStackTrace(); + } + } + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..22895668e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/popup/PopupViewHolder.java @@ -0,0 +1,93 @@ +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/player/popup/StateInterface.java b/app/src/main/java/org/schabi/newpipe/player/popup/StateInterface.java new file mode 100644 index 000000000..94ea41470 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/popup/StateInterface.java @@ -0,0 +1,17 @@ +package org.schabi.newpipe.player.popup; + +public interface StateInterface { + int STATE_LOADING = 123; + int STATE_PLAYING = 125; + int STATE_PAUSED = 126; + int STATE_PAUSED_SEEK = 127; + int STATE_COMPLETED = 128; + + void changeState(int state); + + void onLoading(); + void onPlaying(); + void onPaused(); + void onPausedSeek(); + void onCompleted(); +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java b/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java index 4c43426c5..a5707ecb2 100644 --- a/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java @@ -2,8 +2,12 @@ package org.schabi.newpipe.util; import android.Manifest; import android.app.Activity; +import android.content.Context; +import android.content.Intent; import android.content.pm.PackageManager; +import android.net.Uri; import android.os.Build; +import android.provider.Settings; import android.support.annotation.RequiresApi; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; @@ -11,7 +15,7 @@ import android.support.v4.content.ContextCompat; public class PermissionHelper { public static final int PERMISSION_WRITE_STORAGE = 778; public static final int PERMISSION_READ_STORAGE = 777; - + public static final int PERMISSION_SYSTEM_ALERT_WINDOW = 779; public static boolean checkStoragePermissions(Activity activity) { @@ -65,4 +69,27 @@ public class PermissionHelper { } return true; } + + + /** + * In order to be able to draw over other apps, the permission android.permission.SYSTEM_ALERT_WINDOW have to be granted. + *

+ * On < API 23 (MarshMallow) the permission was granted when the user installed the application (via AndroidManifest), + * on > 23, however, it have to start a activity asking the user if he agree. + *

+ * This method just return if canDraw over other apps, if it doesn't, try to get the permission, + * it does not get the result of the startActivityForResult, if the user accept, the next time that he tries to open + * it will return true. + * + * @param activity context to startActivityForResult + * @return returns {@link Settings#canDrawOverlays(Context)} + **/ + @RequiresApi(api = Build.VERSION_CODES.M) + public static boolean checkSystemAlertWindowPermission(Activity activity) { + if (!Settings.canDrawOverlays(activity)) { + Intent i = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + activity.getPackageName())); + activity.startActivityForResult(i, PERMISSION_SYSTEM_ALERT_WINDOW); + return false; + }else return true; + } } diff --git a/app/src/main/res/drawable-hdpi/ic_action_av_fast_forward.png b/app/src/main/res/drawable-hdpi/ic_action_av_fast_forward.png new file mode 100644 index 0000000000000000000000000000000000000000..672ee37bcbf0fd54d6d6351d6166bea7860c6297 GIT binary patch literal 575 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCmSQK*5Dp-y;YjIVU|{^_>EalY z;rw>`eti~4k>mNTE{iX8dMQrVJ|e$h`PJ21n!;AN#5{Fh?6AmLlS^n~M^K0rmlc=e z5z$BP6Qbu&zjJEVn|Ei{MXP@@`}riT{9gI9z3#c6zQu#z zb>=xvnrCZ{^dA;5S$^@V?KGCIfJS49Hnr)V9nlTx5p8Jthx$pk6lS}7!9%l7$3cb;6_u-M+QvH~Q7kP<-rzm4DK`svG91Z`N3zFBqJ%Q~%O_#?LBOGCO&9MQ8LJ;+w_TVe8-b*tOko!)Y#- zb*G+lPC2lRDe)|``Q!^?bB}&sUb&{>^d3*cx}fDZ#ec3%WJ}#q@3NV#d*1Wd$IDI& zgf{gp;9-9;$E8xFz>!IKgSXJ6DW|{sUC{DbymALe(~K&y4~G^+8@LNC*POINsf=se z#l6ceEagwtJ|g?5?eB!YC9Em)-!;FvoqWC4`;F|0X_1b_lkc7C;YwzGlA0&B#@_D8 zpPkcp>a4%)FLLR_^_OevGmd%e`&gf0^v-4~dQAMk(&&Hx!IJA!w({BnQv`#jtDnm{ Hr-UW|6-NQP literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_replay_white.png b/app/src/main/res/drawable-hdpi/ic_replay_white.png new file mode 100644 index 0000000000000000000000000000000000000000..fcddcf02ddb58ee1680889e7315757d76419b4b2 GIT binary patch literal 675 zcmV;U0$lxxP)i^4O#T$_9m82|5VvpF*wP4OP zM$UZi@^>7mIp;Yu=T~<=&tBi0Ip_J;@t6l6eCkO56W1~dg3&=h1q zQ;-2oK?XDh8PF7DKvR$bO+kjAeDJ~pr|dFMzEwenMqRT?w%I|3a4PK3rz1$XY>?9M z$o%dYv=53msmx64U;8dHjIzKUk9_ChB>7>S;1G`>C)ufEh%>(Om`-|fb*SR^#z4}` zk1=8_ANv5(l+5L85*Xb37G?llB;BT5>A(ui94pbtcshM*TjCYGR_ z-6MNIjB5B%pCT4Cy!N|@y;y<{>^_Y1h{sr_j%UPd_@F#~6P7u_$u$?{J9LdWv<^B( zTt-`dtRg&yta3*15X;emvQ!XfRzVjCmrT?J+75nB>W0d9%nhW=(93#VAeC5$p_bT;=ksFy7&G{R+*d}}W7vaE1LouAaX zWSLB>FM)fQW1AE1DN&IEF;H zzYVeEJLJIAqoJYk(;?%BL(pesxdY8XPo1Z{T&a<9(MT_hOWRs*8a*xy>u=*T0s-0jFCp&d^=VN2rM(KhX;>I$o zX1Z$~$>r#_zL0Kr_~Vw9F$#IkHVxVv(k()a`?ml&dm05_Fj%X{rJTEXdHWr`Umt}l zbu{lc6_ti=KfsqbU6!lu&!e2O%>wJIc-47=pDJcu+;J~>Pv!d%4fZDuGC$PW?OxQq Vp1nUR?JUq=44$rjF6*2UngIOgXq*55 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_replay_white.png b/app/src/main/res/drawable-mdpi/ic_replay_white.png new file mode 100644 index 0000000000000000000000000000000000000000..3b41913257dbe14e552f8203ba4612c9e49fa7a7 GIT binary patch literal 457 zcmV;)0XF`LP)FTt0oJ-PWqJo#>PDsS z`vtg-Yj!6pzlRYn$|uYfju>o)RHCdaOepvx$Asb!d`#= zj9GfM%UQ%Z_%ASSS)uvl>Ee}7Z>bB!D0o=6hwbXB`dMu+HI^KKV^PCw@Y7Xhr@(2=3F!9#Q0L{xh1dvWPHv1m9_7Drj_M4$wROC zE?c)u6RO!;BcznD;^CRl<==#*r(c++^5K)`?B2u@rw?35jLo(!2^Jmnt%V;%zvL6!?*ZKQtC7XY_3Tm%(Wz`S|H%frVxvmCv38R_KRZJgpw0eA>|UgYlP- zKa7`r+UR3+T~*<94NvceC#g51W>4?mT32yris*&g;!1n#RIlBhzUXhAuH3)Y2Z2Hg gj5ttZy~hJ~b~dH59^PfLzy!hI>FVdQ&MBb@08(@E`Tzg` literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_replay_white.png b/app/src/main/res/drawable-xhdpi/ic_replay_white.png new file mode 100644 index 0000000000000000000000000000000000000000..1573fb111b535e5e0df9371ff7f804ce2c40a0fa GIT binary patch literal 908 zcmV;719SX|P)7!(!0)U7tJ0c*i|=W=`GD{Rhrh*S+VSbLJ}_yb|Y} z2=nyPP+9@g9^_WikSR{kYsy-hy zEku#&*XNV6oCC6@Y0~GT-ej9>Czv`dtH=_DbXwRliC~qaN~fj83SwBz>9x4qN(Rvr ze!Z60m#~`DYlFi_98nBIuPvr=tUC1CrC;Y$3W(^FgzQC`p+k!}pOw6jBq=iPfkh0TWmS)dUz^Bf7(L-mV&-4}J$XOF7sBlc6Scm>hhDc(^Uj{LXd-%JWIiC2AM`rMs#_tFGOcEm> zQ6{J?t)HM6bg;rDU%4Peu#|sCR!~qE~FTC?knj22pV}>5;)E~ds^l}5-05`x5 ia0A=`H^2>0aQz3T@7w=K#~w`p0000Eakt z5%+f1-qpg6B1hwoxH*PwxZvj^?YK5aC+9Ewn>D&FYvNP0^VPRCDI}#X?p?ZI?t-le ziegPs8jr$lgg+(ldZv}7zq={^M>l5E?48BuXWoC~US=L0ts~IDz@osw$l<`iL^3nL zA;D_yy)YSFkF|bvuCe?cWx!?Pn$)*{kDIt0cCwH;Zc^>SLOs~uW+y4 zKQG{t%b%be?Qh>rUtf9kI(N$-#(+yIOPix|`woc$1!}@$G_RIqO|6o$ zT`%M{t7U$ddROz#gBDwh;=FDjmgAlxrE%i+>N(zB&zWjAD(qJ~x-#_f<~{k%dyQmS z?>?Po_D#3=!Ih|G_D^=dnmvC`M$@7>&O7(adn-SsKFw%m&6{ty@t%@Zx_I@bL+N!j5rMpAPtH;?_?f`@5^7+?YOV3wd<2|-=>$&6WvR-pOUHZwNU$y}fq)r+ykDi_zRSXO9 zb6X9bT*-QPJxH1P>B3w6TZ|rE$$DzJ=Ny;DA5+6Qe_|36nJ&#Qj@RgOe__2<=}-HW zHx8cVTgpWf^B>NMQrw`_b#CFFtIr?IpT2b8oT#Pq+|CP6`69IUW^38qhB;BMYc8;n immopu_`!eW58@w6gtYIy4&DaLOAMZ_elF{r5}E)|O?+km literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_replay_white.png b/app/src/main/res/drawable-xxhdpi/ic_replay_white.png new file mode 100644 index 0000000000000000000000000000000000000000..5105c2251b72ccf4968f659b10e98200b6a21acd GIT binary patch literal 1390 zcmV-!1(EuRP)BgE0Bosd;&l!5{GB}Nw&aWDv$#(+==h%zxz{0{`dg+v3oNsZEs1O#b7TU=z3 zN+U>6X{oItF;)nPJ2M7_WMc9%Um4!^-ghT8^Ukw#zTbcF!Mpd}bI-XHnCAtLxJ@r7 z*+WWFkh~r@MJBk;QR;*R$?I{j$^*{OAS_5;j{}dp93n0(NM4VF=X6pdEJ$9DgITg9 zR8TMzWb_kr@e}pBNC)$pcHQP(8I2%dRM20TlCA~t5IK}Qkj#7O)Q^3r9eOSQH4a|6K{m=!i&(jnw*l3S} zAo;|Y#mrR|c`AQ73Aj>W`=NK1ad*g$rgcHjacN zsE$?43`9cEp#0VI1I#Q^C$1+=0W+iFH7}gO##wPe=dsZho}dOi%*>M#)$=KDF|$HC z%=VYN*tj4n=n^*Wgxi952pdam7t`|<%h)&=uAn&2u`wd{|1*k>Nn)W2a<^>@q#HZO z!*3+7VHO(;>=dXW7O*kRCla0ZS?o+w8>*fZ|6u2s%rHxW3G57$2u;t&j9}+4Hb_nT zWC$WhETqMGkRZO19H<*2h`taM#pox9o3bOB^}HsC9El2lT!REL$7j+rtL+3cT*2i! z#R$QC8-=m(9KlS|RH2RiIl)|u#=`OrwtD`Z#efJ`%0Sgyv2>j;V3?^)IpJQ zC~$)oVryJ;_A*F;awzhBRG;YTz@uDdxXb~P?psnaT;V0<;<@!m_9(Wo#9CP45&fK^ zm0dKDWCIB{)4(oTIZZ#0Sz#>{I2`q70$a!<#VdblmKEeqBTtav5BJ6{FUU`l0m9~* z`?oM!L%nU}35z-RnM+)dA!%+A1~=V*R#Jl8+0QtYYudtsG%831sUQ`k wf>e+SQb8(6qk>eB3Q|ETNCl}N6{IobUk;_uiK)$-$N&HU07*qoM6N<$g8NE^b^rhX literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_av_fast_forward.png b/app/src/main/res/drawable-xxxhdpi/ic_action_av_fast_forward.png new file mode 100644 index 0000000000000000000000000000000000000000..599c66f70570b6f2668b853354b1d4327aeee514 GIT binary patch literal 1381 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 X>#M$jzV5xv00000NkvXXu0mjfXY*@a literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/popup_controls_bg.xml b/app/src/main/res/drawable/popup_controls_bg.xml new file mode 100644 index 000000000..d04812bd8 --- /dev/null +++ b/app/src/main/res/drawable/popup_controls_bg.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/player_popup.xml b/app/src/main/res/layout/player_popup.xml new file mode 100644 index 000000000..6d1860408 --- /dev/null +++ b/app/src/main/res/layout/player_popup.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/player_popup_notification.xml b/app/src/main/res/layout/player_popup_notification.xml new file mode 100644 index 000000000..188589bef --- /dev/null +++ b/app/src/main/res/layout/player_popup_notification.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/videoitem_detail.xml b/app/src/main/res/menu/videoitem_detail.xml index 06184da1d..c4f2d5fb1 100644 --- a/app/src/main/res/menu/videoitem_detail.xml +++ b/app/src/main/res/menu/videoitem_detail.xml @@ -22,6 +22,10 @@ app:showAsAction="ifRoom" android:icon="?attr/cast"/> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 71c11fd50..263c6bc14 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,6 +11,7 @@ Cancel https://f-droid.org/repository/browse/?fdfilter=vlc&fdid=org.videolan.vlc Open in browser + Open in popup mode Share Loading Download @@ -188,6 +189,7 @@ Copied to clipboard. Please select an available download directory. You have to restart the application to apply the theme.\n\nDo you want to restart now? + This permission is needed to\nopen in popup mode MD5