Implement popup mode
- Add icons replay, fast_forward - Add strings - Add menu entry - Add as option to open link directly to popup mode
@ -7,6 +7,7 @@
|
|||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
@ -175,6 +176,66 @@
|
|||||||
<data android:mimeType="text/plain" />
|
<data android:mimeType="text/plain" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity android:name=".PopupActivity"
|
||||||
|
android:theme="@android:style/Theme.NoDisplay"
|
||||||
|
android:label="NewPipe Popup mode">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="http" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:host="youtube.com" />
|
||||||
|
<data android:host="m.youtube.com" />
|
||||||
|
<data android:host="www.youtube.com" />
|
||||||
|
<!-- video prefix -->
|
||||||
|
<data android:pathPrefix="/v/" />
|
||||||
|
<data android:pathPrefix="/watch" />
|
||||||
|
<data android:pathPrefix="/attribution_link" />
|
||||||
|
<!-- channel prefix -->
|
||||||
|
<data android:pathPrefix="/channel/"/>
|
||||||
|
<data android:pathPrefix="/user/"/>
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="http" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:host="youtu.be" />
|
||||||
|
<data android:pathPrefix="/" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="vnd.youtube" />
|
||||||
|
<data android:scheme="vnd.youtube.launch" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
|
<data android:mimeType="text/plain" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<service android:name=".player.PopupVideoPlayer"/>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
137
app/src/main/java/org/schabi/newpipe/PopupActivity.java
Normal file
@ -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<String> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -53,6 +53,7 @@ class ActionBarHandler {
|
|||||||
// those are edited directly. Typically VideoItemDetailFragment will implement those callbacks.
|
// those are edited directly. Typically VideoItemDetailFragment will implement those callbacks.
|
||||||
private OnActionListener onShareListener;
|
private OnActionListener onShareListener;
|
||||||
private OnActionListener onOpenInBrowserListener;
|
private OnActionListener onOpenInBrowserListener;
|
||||||
|
private OnActionListener onOpenInPopupListener;
|
||||||
private OnActionListener onDownloadListener;
|
private OnActionListener onDownloadListener;
|
||||||
private OnActionListener onPlayWithKodiListener;
|
private OnActionListener onPlayWithKodiListener;
|
||||||
private OnActionListener onPlayAudioListener;
|
private OnActionListener onPlayAudioListener;
|
||||||
@ -190,6 +191,12 @@ class ActionBarHandler {
|
|||||||
activity.startActivity(intent);
|
activity.startActivity(intent);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
case R.id.menu_item_popup: {
|
||||||
|
if(onOpenInPopupListener != null) {
|
||||||
|
onOpenInPopupListener.onActionSelected(selectedVideoStream);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
Log.e(TAG, "Menu Item not known");
|
Log.e(TAG, "Menu Item not known");
|
||||||
}
|
}
|
||||||
@ -208,6 +215,10 @@ class ActionBarHandler {
|
|||||||
onOpenInBrowserListener = listener;
|
onOpenInBrowserListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setOnOpenInPopupListener(OnActionListener listener) {
|
||||||
|
onOpenInPopupListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
public void setOnDownloadListener(OnActionListener listener) {
|
public void setOnDownloadListener(OnActionListener listener) {
|
||||||
onDownloadListener = listener;
|
onDownloadListener = listener;
|
||||||
}
|
}
|
||||||
|
@ -33,8 +33,6 @@ import android.widget.RelativeLayout;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
|
||||||
import com.google.android.exoplayer.util.Util;
|
|
||||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
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.BackgroundPlayer;
|
||||||
import org.schabi.newpipe.player.ExoPlayerActivity;
|
import org.schabi.newpipe.player.ExoPlayerActivity;
|
||||||
import org.schabi.newpipe.player.PlayVideoActivity;
|
import org.schabi.newpipe.player.PlayVideoActivity;
|
||||||
|
import org.schabi.newpipe.player.PopupVideoPlayer;
|
||||||
import org.schabi.newpipe.report.ErrorActivity;
|
import org.schabi.newpipe.report.ErrorActivity;
|
||||||
import java.util.Vector;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.util.NavStack;
|
import org.schabi.newpipe.util.NavStack;
|
||||||
import org.schabi.newpipe.util.PermissionHelper;
|
import org.schabi.newpipe.util.PermissionHelper;
|
||||||
|
|
||||||
|
import java.util.Vector;
|
||||||
|
|
||||||
import static android.app.Activity.RESULT_OK;
|
import static android.app.Activity.RESULT_OK;
|
||||||
import static org.schabi.newpipe.ReCaptchaActivity.RECAPTCHA_REQUEST;
|
import static org.schabi.newpipe.ReCaptchaActivity.RECAPTCHA_REQUEST;
|
||||||
|
|
||||||
@ -324,6 +323,19 @@ public class VideoItemDetailFragment extends Fragment {
|
|||||||
@Override
|
@Override
|
||||||
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
|
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
|
||||||
streamThumbnail = 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
|
@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() {
|
actionBarHandler.setOnPlayWithKodiListener(new ActionBarHandler.OnActionListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onActionSelected(int selectedStreamId) {
|
public void onActionSelected(int selectedStreamId) {
|
||||||
@ -753,13 +787,16 @@ public class VideoItemDetailFragment extends Fragment {
|
|||||||
if (PreferenceManager.getDefaultSharedPreferences(activity)
|
if (PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
.getBoolean(activity.getString(R.string.use_exoplayer_key), false)) {
|
.getBoolean(activity.getString(R.string.use_exoplayer_key), false)) {
|
||||||
|
|
||||||
|
// TODO: Fix this mess
|
||||||
|
if (streamThumbnail != null)
|
||||||
|
ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = streamThumbnail;
|
||||||
// exo player
|
// exo player
|
||||||
|
|
||||||
if(info.dashMpdUrl != null && !info.dashMpdUrl.isEmpty()) {
|
if(info.dashMpdUrl != null && !info.dashMpdUrl.isEmpty()) {
|
||||||
// try dash
|
// try dash
|
||||||
Intent intent = new Intent(activity, ExoPlayerActivity.class)
|
Intent intent = new Intent(activity, ExoPlayerActivity.class)
|
||||||
.setData(Uri.parse(info.dashMpdUrl))
|
.setData(Uri.parse(info.dashMpdUrl));
|
||||||
.putExtra(ExoPlayerActivity.CONTENT_TYPE_EXTRA, Util.TYPE_DASH);
|
//.putExtra(ExoPlayerActivity.CONTENT_TYPE_EXTRA, Util.TYPE_DASH);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
} else if((info.audio_streams != null && !info.audio_streams.isEmpty()) &&
|
} else if((info.audio_streams != null && !info.audio_streams.isEmpty()) &&
|
||||||
(info.video_only_streams != null && !info.video_only_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)
|
Intent intent = new Intent(activity, ExoPlayerActivity.class)
|
||||||
.setDataAndType(Uri.parse(selectedVideoStream.url),
|
.setDataAndType(Uri.parse(selectedVideoStream.url),
|
||||||
MediaFormat.getMimeById(selectedVideoStream.format))
|
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 !!!
|
activity.startActivity(intent); // HERE !!!
|
||||||
}
|
}
|
||||||
|
@ -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.
|
|
||||||
* <p>
|
|
||||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
|
||||||
* ExoPlayerActivity.java is part of NewPipe. all changes are under GPL3
|
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* <p>
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.schabi.newpipe.player;
|
package org.schabi.newpipe.player;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
@ -56,6 +20,7 @@ import org.schabi.newpipe.R;
|
|||||||
|
|
||||||
public class ExoPlayerActivity extends Activity implements OnPreparedListener, OnCompletionListener {
|
public class ExoPlayerActivity extends Activity implements OnPreparedListener, OnCompletionListener {
|
||||||
private static final String TAG = "ExoPlayerActivity";
|
private static final String TAG = "ExoPlayerActivity";
|
||||||
|
private static final boolean DEBUG = false;
|
||||||
private EMVideoView videoView;
|
private EMVideoView videoView;
|
||||||
private CustomVideoControls videoControls;
|
private CustomVideoControls videoControls;
|
||||||
|
|
||||||
@ -94,13 +59,13 @@ public class ExoPlayerActivity extends Activity implements OnPreparedListener, O
|
|||||||
videoControls.setVisibilityListener(new VideoControlsVisibilityListener() {
|
videoControls.setVisibilityListener(new VideoControlsVisibilityListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onControlsShown() {
|
public void onControlsShown() {
|
||||||
Log.d(TAG, "------------ onControlsShown() called");
|
if (DEBUG) Log.d(TAG, "------------ onControlsShown() called");
|
||||||
showSystemUi();
|
showSystemUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onControlsHidden() {
|
public void onControlsHidden() {
|
||||||
Log.d(TAG, "------------ onControlsHidden() called");
|
if (DEBUG) Log.d(TAG, "------------ onControlsHidden() called");
|
||||||
hideSystemUi();
|
hideSystemUi();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -109,13 +74,13 @@ public class ExoPlayerActivity extends Activity implements OnPreparedListener, O
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPrepared() {
|
public void onPrepared() {
|
||||||
Log.d(TAG, "onPrepared() called");
|
if (DEBUG) Log.d(TAG, "onPrepared() called");
|
||||||
videoView.start();
|
videoView.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCompletion() {
|
public void onCompletion() {
|
||||||
Log.d(TAG, "onCompletion() called");
|
if (DEBUG) Log.d(TAG, "onCompletion() called");
|
||||||
// videoView.getVideoControls().setButtonListener();
|
// videoView.getVideoControls().setButtonListener();
|
||||||
//videoView.restart();
|
//videoView.restart();
|
||||||
videoControls.setRewindButtonRemoved(true);
|
videoControls.setRewindButtonRemoved(true);
|
||||||
@ -144,13 +109,13 @@ public class ExoPlayerActivity extends Activity implements OnPreparedListener, O
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void showSystemUi() {
|
private void showSystemUi() {
|
||||||
Log.d(TAG, "showSystemUi() called");
|
if (DEBUG) Log.d(TAG, "showSystemUi() called");
|
||||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||||
getWindow().getDecorView().setSystemUiVisibility(0);
|
getWindow().getDecorView().setSystemUiVisibility(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void hideSystemUi() {
|
private void hideSystemUi() {
|
||||||
Log.d(TAG, "hideSystemUi() called");
|
if (DEBUG) Log.d(TAG, "hideSystemUi() called");
|
||||||
if (android.os.Build.VERSION.SDK_INT >= 17) {
|
if (android.os.Build.VERSION.SDK_INT >= 17) {
|
||||||
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
@ -234,7 +199,7 @@ public class ExoPlayerActivity extends Activity implements OnPreparedListener, O
|
|||||||
protected void onPlayPauseClick() {
|
protected void onPlayPauseClick() {
|
||||||
super.onPlayPauseClick();
|
super.onPlayPauseClick();
|
||||||
if (videoView == null) return;
|
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) {
|
if (isFinished) {
|
||||||
videoView.restart();
|
videoView.restart();
|
||||||
setRewindButtonRemoved(false);
|
setRewindButtonRemoved(false);
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
@ -2,8 +2,12 @@ package org.schabi.newpipe.util;
|
|||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.provider.Settings;
|
||||||
import android.support.annotation.RequiresApi;
|
import android.support.annotation.RequiresApi;
|
||||||
import android.support.v4.app.ActivityCompat;
|
import android.support.v4.app.ActivityCompat;
|
||||||
import android.support.v4.content.ContextCompat;
|
import android.support.v4.content.ContextCompat;
|
||||||
@ -11,7 +15,7 @@ import android.support.v4.content.ContextCompat;
|
|||||||
public class PermissionHelper {
|
public class PermissionHelper {
|
||||||
public static final int PERMISSION_WRITE_STORAGE = 778;
|
public static final int PERMISSION_WRITE_STORAGE = 778;
|
||||||
public static final int PERMISSION_READ_STORAGE = 777;
|
public static final int PERMISSION_READ_STORAGE = 777;
|
||||||
|
public static final int PERMISSION_SYSTEM_ALERT_WINDOW = 779;
|
||||||
|
|
||||||
|
|
||||||
public static boolean checkStoragePermissions(Activity activity) {
|
public static boolean checkStoragePermissions(Activity activity) {
|
||||||
@ -65,4 +69,27 @@ public class PermissionHelper {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In order to be able to draw over other apps, the permission android.permission.SYSTEM_ALERT_WINDOW have to be granted.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
BIN
app/src/main/res/drawable-hdpi/ic_action_av_fast_forward.png
Normal file
After Width: | Height: | Size: 575 B |
BIN
app/src/main/res/drawable-hdpi/ic_replay_white.png
Normal file
After Width: | Height: | Size: 675 B |
BIN
app/src/main/res/drawable-mdpi/ic_action_av_fast_forward.png
Normal file
After Width: | Height: | Size: 277 B |
BIN
app/src/main/res/drawable-mdpi/ic_replay_white.png
Normal file
After Width: | Height: | Size: 457 B |
BIN
app/src/main/res/drawable-xhdpi/ic_action_av_fast_forward.png
Normal file
After Width: | Height: | Size: 574 B |
BIN
app/src/main/res/drawable-xhdpi/ic_replay_white.png
Normal file
After Width: | Height: | Size: 908 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_action_av_fast_forward.png
Normal file
After Width: | Height: | Size: 889 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_replay_white.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_action_av_fast_forward.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_replay_white.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
9
app/src/main/res/drawable/popup_controls_bg.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<gradient
|
||||||
|
android:startColor="#a0000000"
|
||||||
|
android:centerColor="#26000000"
|
||||||
|
android:endColor="#00000000"
|
||||||
|
android:angle="90"
|
||||||
|
/>
|
||||||
|
</shape>
|
110
app/src/main/res/layout/player_popup.xml
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:id="@+id/blackBackground"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/black"/>
|
||||||
|
|
||||||
|
<com.devbrackets.android.exomedia.ui.widget.EMVideoView
|
||||||
|
android:id="@+id/popupVideoView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"/>
|
||||||
|
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/endScreen"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/black"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible"
|
||||||
|
tools:background="@android:color/white"
|
||||||
|
tools:ignore="ContentDescription"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:weightSum="2">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/controlAnimationView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:src="@drawable/ic_action_av_fast_rewind"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible"
|
||||||
|
tools:ignore="ContentDescription"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/playbackControlRoot"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:paddingBottom="5dp"
|
||||||
|
android:paddingLeft="8dp"
|
||||||
|
android:paddingRight="8dp"
|
||||||
|
android:gravity="bottom|center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:background="@drawable/popup_controls_bg"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/playbackCurrentTime"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingLeft="2dp"
|
||||||
|
android:paddingRight="2dp"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
tools:ignore="HardcodedText"
|
||||||
|
android:text="-:--:--"/>
|
||||||
|
|
||||||
|
<!--style="?android:attr/progressBarStyleHorizontal"-->
|
||||||
|
<SeekBar
|
||||||
|
android:id="@+id/playbackSeekBar"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:max="100"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:progress="0"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/playbackEndTime"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingLeft="2dp"
|
||||||
|
android:paddingRight="2dp"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
tools:ignore="HardcodedText"
|
||||||
|
android:text="-:--:--"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:id="@+id/loadingPanel"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:background="@color/black"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="20dp"
|
||||||
|
tools:visibility="gone">
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
style="?android:attr/progressBarStyleLarge"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:indeterminate="true"/>
|
||||||
|
</RelativeLayout>
|
||||||
|
</FrameLayout>
|
69
app/src/main/res/layout/player_popup_notification.xml
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/notificationContent"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="64dp"
|
||||||
|
android:background="@color/background_notification_color"
|
||||||
|
android:clickable="true"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/notificationCover"
|
||||||
|
android:layout_width="64dp"
|
||||||
|
android:layout_height="64dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:src="@drawable/dummy_thumbnail"
|
||||||
|
tools:ignore="ContentDescription"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:ignore="RtlHardcoded">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/notificationSongName"
|
||||||
|
style="@android:style/TextAppearance.StatusBar.EventContent.Title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:maxLines="1"
|
||||||
|
tools:text="title"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/notificationArtist"
|
||||||
|
style="@android:style/TextAppearance.StatusBar.EventContent"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:maxLines="1"
|
||||||
|
tools:text="artist"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/notificationPlayPause"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_margin="5dp"
|
||||||
|
android:background="#00ffffff"
|
||||||
|
android:clickable="true"
|
||||||
|
android:scaleType="fitXY"
|
||||||
|
android:src="@drawable/ic_pause_white_24dp"
|
||||||
|
tools:ignore="ContentDescription"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/notificationStop"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_margin="5dp"
|
||||||
|
android:background="#00ffffff"
|
||||||
|
android:clickable="true"
|
||||||
|
android:scaleType="fitXY"
|
||||||
|
android:src="@drawable/ic_close_white_24dp"
|
||||||
|
tools:ignore="ContentDescription"/>
|
||||||
|
</LinearLayout>
|
@ -22,6 +22,10 @@
|
|||||||
app:showAsAction="ifRoom"
|
app:showAsAction="ifRoom"
|
||||||
android:icon="?attr/cast"/>
|
android:icon="?attr/cast"/>
|
||||||
|
|
||||||
|
<item android:id="@+id/menu_item_popup"
|
||||||
|
app:showAsAction="never"
|
||||||
|
android:title="@string/open_in_popup_mode"/>
|
||||||
|
|
||||||
<item android:id="@+id/menu_item_openInBrowser"
|
<item android:id="@+id/menu_item_openInBrowser"
|
||||||
app:showAsAction="never"
|
app:showAsAction="never"
|
||||||
android:title="@string/open_in_browser" />
|
android:title="@string/open_in_browser" />
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
<string name="cancel">Cancel</string>
|
<string name="cancel">Cancel</string>
|
||||||
<string name="fdroid_vlc_url" translatable="false">https://f-droid.org/repository/browse/?fdfilter=vlc&fdid=org.videolan.vlc</string>
|
<string name="fdroid_vlc_url" translatable="false">https://f-droid.org/repository/browse/?fdfilter=vlc&fdid=org.videolan.vlc</string>
|
||||||
<string name="open_in_browser">Open in browser</string>
|
<string name="open_in_browser">Open in browser</string>
|
||||||
|
<string name="open_in_popup_mode">Open in popup mode</string>
|
||||||
<string name="share">Share</string>
|
<string name="share">Share</string>
|
||||||
<string name="loading">Loading</string>
|
<string name="loading">Loading</string>
|
||||||
<string name="download">Download</string>
|
<string name="download">Download</string>
|
||||||
@ -188,6 +189,7 @@
|
|||||||
<string name="msg_copied">Copied to clipboard.</string>
|
<string name="msg_copied">Copied to clipboard.</string>
|
||||||
<string name="no_available_dir">Please select an available download directory.</string>
|
<string name="no_available_dir">Please select an available download directory.</string>
|
||||||
<string name="msg_restart">You have to restart the application to apply the theme.\n\nDo you want to restart now?</string>
|
<string name="msg_restart">You have to restart the application to apply the theme.\n\nDo you want to restart now?</string>
|
||||||
|
<string name="msg_popup_permission">This permission is needed to\nopen in popup mode</string>
|
||||||
|
|
||||||
<!-- Checksum types -->
|
<!-- Checksum types -->
|
||||||
<string name="md5" translatable="false">MD5</string>
|
<string name="md5" translatable="false">MD5</string>
|
||||||
|