Merge branch 'player-update' into 'develop'

Player update

See merge request sschueller/peertube!46
This commit is contained in:
Stefan Schüller 2022-01-01 00:53:43 +00:00
commit 91b4869934
51 changed files with 3426 additions and 2390 deletions

View File

@ -8,7 +8,7 @@ ENV ANDROID_SDK_CHECKSUM 124f2d5115eee365df6cf3228ffbca6fc3911d16f8025bebd5b1c6e
# higher version casues Warning: Failed to find package
ENV ANDROID_BUILD_TOOLS_VERSION 30.0.2
ENV ANDROID_SDK_ROOT /usr/local/android-sdk-linux
ENV ANDROID_VERSION 30
ENV ANDROID_VERSION 32
# ENV PATH ${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools
ENV PATH ${PATH}:${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin:${ANDROID_SDK_ROOT}/cmdline-tools/tools/bin

View File

@ -39,13 +39,13 @@ else {
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 30
compileSdkVersion 32
buildToolsVersion "30.0.2"
defaultConfig {
applicationId "net.schueller.peertube"
minSdkVersion 21
targetSdkVersion 30
targetSdkVersion 32
versionCode 1069
versionName "1.8.3"
buildConfigField "long", "BUILD_TIME", readPropertyWithDefault('buildTimestamp', System.currentTimeMillis()) + 'L'
@ -94,10 +94,10 @@ android {
}
def room_version = "2.3.0"
def lifecycleVersion = '2.3.1'
def exoplayer = '2.12.3'
def fragment_version = "1.3.6"
def room_version = "2.4.0"
def lifecycleVersion = '2.4.0'
def exoplayer = '2.16.1'
def fragment_version = "1.4.0"
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
@ -105,8 +105,8 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// Layouts and design
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
implementation 'androidx.appcompat:appcompat:1.4.0'
implementation 'com.google.android.material:material:1.4.0'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
@ -118,7 +118,7 @@ dependencies {
implementation 'com.mikepenz:fontawesome-typeface:5.9.0.2-kotlin@aar'
// http client / REST
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
implementation 'com.squareup.okhttp3:okhttp:4.9.2'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
// image downloading and caching library

View File

@ -41,7 +41,7 @@ import java.util.*
class ServerAddressBookActivity : CommonActivity() {
private val TAG = "ServerAddressBookActivity"
private val TAG = "ServerAddBookAct"
private val mServerViewModel: ServerViewModel by viewModels()
private var addServerFragment: AddServerFragment? = null
@ -133,15 +133,15 @@ class ServerAddressBookActivity : CommonActivity() {
AlertDialog.Builder(this@ServerAddressBookActivity)
.setTitle(getString(R.string.server_book_del_alert_title))
.setMessage(getString(R.string.server_book_del_alert_msg))
.setPositiveButton(android.R.string.yes) { _: DialogInterface?, _: Int ->
val position = viewHolder.adapterPosition
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
val position = viewHolder.bindingAdapterPosition
val server = adapter.getServerAtPosition(position)
// Toast.makeText(ServerAddressBookActivity.this, "Deleting " +
// server.getServerName(), Toast.LENGTH_LONG).show();
// Delete the server
mServerViewModel.delete(server)
}
.setNegativeButton(android.R.string.no) { _: DialogInterface?, _: Int -> adapter.notifyItemChanged(viewHolder.adapterPosition) }
.setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int -> adapter.notifyItemChanged(viewHolder.bindingAdapterPosition) }
.setIcon(android.R.drawable.ic_dialog_alert)
.show()
}

View File

@ -19,6 +19,7 @@ package net.schueller.peertube.activity
import android.Manifest.permission
import android.R.drawable
import android.R.string
import android.app.Activity
import android.app.AlertDialog.Builder
import android.app.SearchManager
import android.content.Context
@ -33,6 +34,7 @@ import android.view.MenuItem
import android.view.MenuItem.OnActionExpandListener
import android.view.View
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.SearchView.OnSuggestionListener
import androidx.appcompat.widget.Toolbar
@ -116,7 +118,7 @@ class VideoListActivity : CommonActivity() {
Builder(this@VideoListActivity)
.setTitle(getString(R.string.clear_search_history))
.setMessage(getString(R.string.clear_search_history_prompt))
.setPositiveButton(string.yes) { _, _ ->
.setPositiveButton(string.ok) { _, _ ->
val suggestions = SearchRecentSuggestions(
applicationContext,
SearchSuggestionsProvider.AUTHORITY,
@ -124,7 +126,7 @@ class VideoListActivity : CommonActivity() {
)
suggestions.clearHistory()
}
.setNegativeButton(string.no, null)
.setNegativeButton(string.cancel, null)
.setIcon(drawable.ic_dialog_alert)
.show()
true
@ -160,8 +162,7 @@ class VideoListActivity : CommonActivity() {
position
) as Cursor
return cursor.getString(
cursor
.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1)
cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1)
)
}
@ -178,15 +179,26 @@ class VideoListActivity : CommonActivity() {
stopService(Intent(this, VideoPlayerService::class.java))
}
public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == SWITCH_INSTANCE) {
if (resultCode == RESULT_OK) {
loadVideos(currentStart, count, sort, filter)
}
// public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
// super.onActivityResult(requestCode, resultCode, data)
// if (requestCode == SWITCH_INSTANCE) {
// if (resultCode == RESULT_OK) {
// loadVideos(currentStart, count, sort, filter)
// }
// }
// }
private var resultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
loadVideos(currentStart, count, sort, filter)
}
}
private fun openActivityForResult(intent: Intent) {
resultLauncher.launch(intent)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
@ -213,7 +225,7 @@ class VideoListActivity : CommonActivity() {
}
id.action_server_address_book -> {
val addressBookActivityIntent = Intent(this, ServerAddressBookActivity::class.java)
this.startActivityForResult(addressBookActivityIntent, SWITCH_INSTANCE)
openActivityForResult(addressBookActivityIntent)
return false
}
else -> {
@ -461,7 +473,7 @@ class VideoListActivity : CommonActivity() {
// new IconicsDrawable(this, FontAwesome.Icon.faw_user_circle));
// Click Listener
navigation.setOnNavigationItemSelectedListener { menuItem: MenuItem ->
navigation.setOnItemSelectedListener { menuItem: MenuItem ->
when (menuItem.itemId) {
id.navigation_overview -> {
// TODO
@ -470,7 +482,7 @@ class VideoListActivity : CommonActivity() {
loadOverview(currentPage)
overViewActive = true
}
return@setOnNavigationItemSelectedListener true
return@setOnItemSelectedListener true
}
id.navigation_trending -> {
//Log.v(TAG, "navigation_trending");
@ -482,7 +494,7 @@ class VideoListActivity : CommonActivity() {
subscriptions = false
loadVideos(currentStart, count, sort, filter)
}
return@setOnNavigationItemSelectedListener true
return@setOnItemSelectedListener true
}
id.navigation_recent -> {
if (!isLoading) {
@ -493,7 +505,7 @@ class VideoListActivity : CommonActivity() {
subscriptions = false
loadVideos(currentStart, count, sort, filter)
}
return@setOnNavigationItemSelectedListener true
return@setOnItemSelectedListener true
}
id.navigation_local -> {
//Log.v(TAG, "navigation_trending");
@ -505,15 +517,15 @@ class VideoListActivity : CommonActivity() {
subscriptions = false
loadVideos(currentStart, count, sort, filter)
}
return@setOnNavigationItemSelectedListener true
return@setOnItemSelectedListener true
}
id.navigation_subscriptions -> //Log.v(TAG, "navigation_subscriptions");
if (!Session.getInstance().isLoggedIn) {
// Intent intent = new Intent(this, LoginActivity.class);
// this.startActivity(intent);
val addressBookActivityIntent = Intent(this, ServerAddressBookActivity::class.java)
this.startActivityForResult(addressBookActivityIntent, SWITCH_INSTANCE)
return@setOnNavigationItemSelectedListener false
openActivityForResult(addressBookActivityIntent)
return@setOnItemSelectedListener false
} else {
if (!isLoading) {
overViewActive = false
@ -523,7 +535,7 @@ class VideoListActivity : CommonActivity() {
subscriptions = true
loadVideos(currentStart, count, sort, filter)
}
return@setOnNavigationItemSelectedListener true
return@setOnItemSelectedListener true
}
}
false
@ -574,6 +586,5 @@ class VideoListActivity : CommonActivity() {
const val EXTRA_VIDEOID = "VIDEOID"
const val EXTRA_ACCOUNTDISPLAYNAME = "ACCOUNTDISPLAYNAMEANDHOST"
const val SWITCH_INSTANCE = 2
}
}

View File

@ -1,502 +0,0 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.activity;
import android.annotation.SuppressLint;
import android.app.AppOpsManager;
import android.app.PendingIntent;
import android.app.PictureInPictureParams;
import android.app.RemoteAction;
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.drawable.Icon;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import android.text.TextUtils;
import android.util.Log;
import android.util.Rational;
import android.util.TypedValue;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import net.schueller.peertube.R;
import net.schueller.peertube.fragment.VideoMetaDataFragment;
import net.schueller.peertube.fragment.VideoPlayerFragment;
import net.schueller.peertube.service.VideoPlayerService;
import java.util.ArrayList;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import static com.google.android.exoplayer2.ui.PlayerNotificationManager.ACTION_PAUSE;
import static com.google.android.exoplayer2.ui.PlayerNotificationManager.ACTION_PLAY;
import static com.google.android.exoplayer2.ui.PlayerNotificationManager.ACTION_STOP;
import static net.schueller.peertube.helper.VideoHelper.canEnterPipMode;
public class VideoPlayActivity extends AppCompatActivity {
private static final String TAG = "VideoPlayActivity";
static boolean floatMode = false;
private static final int REQUEST_CODE = 101;
private BroadcastReceiver receiver;
//This can only be called when in entering pip mode which can't happen if the device doesn't support pip mode.
@SuppressLint("NewApi")
public void makePipControls() {
FragmentManager fragmentManager = getSupportFragmentManager();
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById(R.id.video_player_fragment);
ArrayList<RemoteAction> actions = new ArrayList<>();
Intent actionIntent = new Intent(getString(R.string.app_background_audio));
PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), REQUEST_CODE, actionIntent, 0);
@SuppressLint({"NewApi", "LocalSuppress"}) Icon icon = Icon.createWithResource(getApplicationContext(), android.R.drawable.stat_sys_speakerphone);
@SuppressLint({"NewApi", "LocalSuppress"}) RemoteAction remoteAction = new RemoteAction(icon, "close pip", "from pip window custom command", pendingIntent);
actions.add(remoteAction);
actionIntent = new Intent(ACTION_STOP);
pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), REQUEST_CODE, actionIntent, 0);
icon = Icon.createWithResource(getApplicationContext(), com.google.android.exoplayer2.ui.R.drawable.exo_notification_stop);
remoteAction = new RemoteAction(icon, "play", "stop the media", pendingIntent);
actions.add(remoteAction);
assert videoPlayerFragment != null;
if (videoPlayerFragment.isPaused()) {
Log.e(TAG, "setting actions with play button");
actionIntent = new Intent(ACTION_PLAY);
pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), REQUEST_CODE, actionIntent, 0);
icon = Icon.createWithResource(getApplicationContext(), com.google.android.exoplayer2.ui.R.drawable.exo_notification_play);
remoteAction = new RemoteAction(icon, "play", "play the media", pendingIntent);
} else {
Log.e(TAG, "setting actions with pause button");
actionIntent = new Intent(ACTION_PAUSE);
pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), REQUEST_CODE, actionIntent, 0);
icon = Icon.createWithResource(getApplicationContext(), com.google.android.exoplayer2.ui.R.drawable.exo_notification_pause);
remoteAction = new RemoteAction(icon, "pause", "pause the media", pendingIntent);
}
actions.add(remoteAction);
//add custom actions to pip window
PictureInPictureParams params =
new PictureInPictureParams.Builder()
.setActions(actions)
.build();
setPictureInPictureParams(params);
}
public void changedToPipMode() {
FragmentManager fragmentManager = getSupportFragmentManager();
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById(R.id.video_player_fragment);
assert videoPlayerFragment != null;
videoPlayerFragment.showControls(false);
//create custom actions
makePipControls();
//setup receiver to handle customer actions
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_STOP);
filter.addAction(ACTION_PAUSE);
filter.addAction(ACTION_PLAY);
filter.addAction((getString(R.string.app_background_audio)));
receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
assert action != null;
if (action.equals(ACTION_PAUSE)) {
videoPlayerFragment.pauseVideo();
makePipControls();
}
if (action.equals(ACTION_PLAY)) {
videoPlayerFragment.unPauseVideo();
makePipControls();
}
if (action.equals(getString(R.string.app_background_audio))) {
unregisterReceiver(receiver);
finish();
}
if (action.equals(ACTION_STOP)) {
unregisterReceiver(receiver);
finishAndRemoveTask();
}
}
};
registerReceiver(receiver, filter);
Log.v(TAG, "switched to pip ");
floatMode = true;
videoPlayerFragment.showControls(false);
}
public void changedToNormalMode() {
FragmentManager fragmentManager = getSupportFragmentManager();
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById(R.id.video_player_fragment);
assert videoPlayerFragment != null;
videoPlayerFragment.showControls(true);
if (receiver != null) {
unregisterReceiver(receiver);
}
Log.v(TAG, "switched to normal");
floatMode = false;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set theme
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
setTheme(getResources().getIdentifier(
sharedPref.getString(
getString(R.string.pref_theme_key),
getString(R.string.app_default_theme)
),
"style",
getPackageName())
);
setContentView(R.layout.activity_video_play);
// get video ID
Intent intent = getIntent();
String videoUuid = intent.getStringExtra(VideoListActivity.EXTRA_VIDEOID);
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment)
getSupportFragmentManager().findFragmentById(R.id.video_player_fragment);
assert videoPlayerFragment != null;
String playingVideo = videoPlayerFragment.getVideoUuid();
Log.v(TAG, "oncreate click: " + videoUuid + " is trying to replace: " + playingVideo);
if (TextUtils.isEmpty(playingVideo)) {
Log.v(TAG, "oncreate no video currently playing");
videoPlayerFragment.start(videoUuid);
} else if (!playingVideo.equals(videoUuid)) {
Log.v(TAG, "oncreate different video playing currently");
videoPlayerFragment.stopVideo();
videoPlayerFragment.start(videoUuid);
} else {
Log.v(TAG, "oncreate same video playing currently");
}
// if we are in landscape set the video to fullscreen
int orientation = this.getResources().getConfiguration().orientation;
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
setOrientation(true);
}
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment)
getSupportFragmentManager().findFragmentById(R.id.video_player_fragment);
assert videoPlayerFragment != null;
String videoUuid = intent.getStringExtra(VideoListActivity.EXTRA_VIDEOID);
Log.v(TAG, "new intent click: " + videoUuid + " is trying to replace: " + videoPlayerFragment.getVideoUuid());
String playingVideo = videoPlayerFragment.getVideoUuid();
if (TextUtils.isEmpty(playingVideo)) {
Log.v(TAG, "new intent no video currently playing");
videoPlayerFragment.start(videoUuid);
} else if (!playingVideo.equals(videoUuid)) {
Log.v(TAG, "new intent different video playing currently");
videoPlayerFragment.stopVideo();
videoPlayerFragment.start(videoUuid);
} else {
Log.v(TAG, "new intent same video playing currently");
}
// if we are in landscape set the video to fullscreen
int orientation = this.getResources().getConfiguration().orientation;
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
setOrientation(true);
}
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
Log.v(TAG, "onConfigurationChanged()...");
super.onConfigurationChanged(newConfig);
// Checking the orientation changes of the screen
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
setOrientation(true);
} else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
setOrientation(false);
}
}
private void setOrientation(Boolean isLandscape) {
FragmentManager fragmentManager = getSupportFragmentManager();
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById(R.id.video_player_fragment);
VideoMetaDataFragment videoMetaFragment = (VideoMetaDataFragment) fragmentManager.findFragmentById(R.id.video_meta_data_fragment);
assert videoPlayerFragment != null;
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) videoPlayerFragment.requireView().getLayoutParams();
params.width = FrameLayout.LayoutParams.MATCH_PARENT;
params.height = isLandscape ? FrameLayout.LayoutParams.MATCH_PARENT : (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 250, getResources().getDisplayMetrics());
videoPlayerFragment.requireView().setLayoutParams(params);
if (videoMetaFragment != null) {
FragmentTransaction transaction = fragmentManager.beginTransaction()
.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out);
if (isLandscape) {
transaction.hide(videoMetaFragment);
} else {
transaction.show(videoMetaFragment);
}
transaction.commit();
}
videoPlayerFragment.setIsFullscreen(isLandscape);
if ( isLandscape ) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
}
@Override
protected void onDestroy() {
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment)
getSupportFragmentManager().findFragmentById(R.id.video_player_fragment);
assert videoPlayerFragment != null;
videoPlayerFragment.destroyVideo();
super.onDestroy();
Log.v(TAG, "onDestroy...");
}
@Override
protected void onPause() {
super.onPause();
Log.v(TAG, "onPause()...");
}
@Override
protected void onResume() {
super.onResume();
Log.v(TAG, "onResume()...");
}
@Override
protected void onStop() {
super.onStop();
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment)
getSupportFragmentManager().findFragmentById(R.id.video_player_fragment);
assert videoPlayerFragment != null;
videoPlayerFragment.stopVideo();
Log.v(TAG, "onStop()...");
}
@Override
protected void onStart() {
super.onStart();
Log.v(TAG, "onStart()...");
}
@SuppressLint("NewApi")
@Override
public void onUserLeaveHint() {
Log.v(TAG, "onUserLeaveHint()...");
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
FragmentManager fragmentManager = getSupportFragmentManager();
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById(R.id.video_player_fragment);
VideoMetaDataFragment videoMetaDataFragment = (VideoMetaDataFragment) fragmentManager.findFragmentById(R.id.video_meta_data_fragment);
String backgroundBehavior = sharedPref.getString(getString(R.string.pref_background_behavior_key), getString(R.string.pref_background_stop_key));
assert videoPlayerFragment != null;
assert backgroundBehavior != null;
if ( videoMetaDataFragment.isLeaveAppExpected() )
{
super.onUserLeaveHint();
return;
}
if (backgroundBehavior.equals(getString(R.string.pref_background_stop_key))) {
Log.v(TAG, "stop the video");
videoPlayerFragment.pauseVideo();
stopService(new Intent(this, VideoPlayerService.class));
super.onBackPressed();
} else if (backgroundBehavior.equals(getString(R.string.pref_background_audio_key))) {
Log.v(TAG, "play the Audio");
super.onBackPressed();
} else if (backgroundBehavior.equals(getString(R.string.pref_background_float_key))) {
Log.v(TAG, "play in floating video");
//canEnterPIPMode makes sure API level is high enough
if (canEnterPipMode(this)) {
Log.v(TAG, "enabling pip");
enterPipMode();
} else {
Log.v(TAG, "unable to use pip");
}
} else {
// Deal with bad entries from older version
Log.v(TAG, "No setting, fallback");
super.onBackPressed();
}
}
// @RequiresApi(api = Build.VERSION_CODES.O)
@SuppressLint("NewApi")
public void onBackPressed() {
Log.v(TAG, "onBackPressed()...");
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment)
getSupportFragmentManager().findFragmentById(R.id.video_player_fragment);
assert videoPlayerFragment != null;
// copying Youtube behavior to have back button exit full screen.
if (videoPlayerFragment.getIsFullscreen()) {
Log.v(TAG, "exiting full screen");
videoPlayerFragment.fullScreenToggle();
return;
}
// pause video if pref is enabled
if (sharedPref.getBoolean(getString(R.string.pref_back_pause_key), true)) {
videoPlayerFragment.pauseVideo();
}
String backgroundBehavior = sharedPref.getString(getString(R.string.pref_background_behavior_key), getString(R.string.pref_background_stop_key));
assert backgroundBehavior != null;
if (backgroundBehavior.equals(getString(R.string.pref_background_stop_key))) {
Log.v(TAG, "stop the video");
videoPlayerFragment.pauseVideo();
stopService(new Intent(this, VideoPlayerService.class));
super.onBackPressed();
} else if (backgroundBehavior.equals(getString(R.string.pref_background_audio_key))) {
Log.v(TAG, "play the Audio");
super.onBackPressed();
} else if (backgroundBehavior.equals(getString(R.string.pref_background_float_key))) {
Log.v(TAG, "play in floating video");
//canEnterPIPMode makes sure API level is high enough
if (canEnterPipMode(this)) {
Log.v(TAG, "enabling pip");
enterPipMode();
//fixes problem where back press doesn't bring up video list after returning from PIP mode
Intent intentSettings = new Intent(this, VideoListActivity.class);
this.startActivity(intentSettings);
} else {
Log.v(TAG, "Unable to enter PIP mode");
super.onBackPressed();
}
} else {
// Deal with bad entries from older version
Log.v(TAG, "No setting, fallback");
super.onBackPressed();
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
public void enterPipMode() {
final FragmentManager fragmentManager = getSupportFragmentManager();
final VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById( R.id.video_player_fragment );
if ( videoPlayerFragment.getVideoAspectRatio() == 0 ) {
Log.i( TAG, "impossible to switch to pip" );
} else {
Rational rational = new Rational( (int) ( videoPlayerFragment.getVideoAspectRatio() * 100 ), 100 );
PictureInPictureParams mParams =
new PictureInPictureParams.Builder()
.setAspectRatio( rational )
// .setSourceRectHint(new Rect(0,500,400,600))
.build();
enterPictureInPictureMode( mParams );
}
}
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
FragmentManager fragmentManager = getSupportFragmentManager();
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById(R.id.video_player_fragment);
if (videoPlayerFragment != null) {
if (isInPictureInPictureMode) {
changedToPipMode();
Log.v(TAG, "switched to pip ");
videoPlayerFragment.useController(false);
} else {
changedToNormalMode();
Log.v(TAG, "switched to normal");
videoPlayerFragment.useController(true);
}
} else {
Log.e(TAG, "videoPlayerFragment is NULL");
}
}
}

View File

@ -0,0 +1,461 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.activity
import androidx.appcompat.app.AppCompatActivity
import android.annotation.SuppressLint
import net.schueller.peertube.fragment.VideoPlayerFragment
import net.schueller.peertube.R
import android.app.RemoteAction
import android.app.PendingIntent
import com.google.android.exoplayer2.ui.PlayerNotificationManager
import android.app.PictureInPictureParams
import android.content.*
import android.content.res.Configuration
import android.graphics.drawable.Icon
import android.os.Bundle
import android.text.TextUtils
import net.schueller.peertube.fragment.VideoMetaDataFragment
import android.widget.RelativeLayout
import android.widget.FrameLayout
import android.util.TypedValue
import android.view.WindowManager
import net.schueller.peertube.service.VideoPlayerService
import net.schueller.peertube.helper.VideoHelper
import androidx.annotation.RequiresApi
import android.os.Build
import android.util.Log
import android.util.Rational
import androidx.fragment.app.Fragment
import net.schueller.peertube.fragment.VideoDescriptionFragment
import java.util.ArrayList
class VideoPlayActivity : CommonActivity() {
private var receiver: BroadcastReceiver? = null
//This can only be called when in entering pip mode which can't happen if the device doesn't support pip mode.
@SuppressLint("NewApi")
fun makePipControls() {
val fragmentManager = supportFragmentManager
val videoPlayerFragment =
fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?
val actions = ArrayList<RemoteAction>()
var actionIntent = Intent(getString(R.string.app_background_audio))
var pendingIntent =
PendingIntent.getBroadcast(applicationContext, REQUEST_CODE, actionIntent, 0)
@SuppressLint("NewApi", "LocalSuppress") var icon = Icon.createWithResource(
applicationContext, android.R.drawable.stat_sys_speakerphone
)
@SuppressLint("NewApi", "LocalSuppress") var remoteAction =
RemoteAction(icon!!, "close pip", "from pip window custom command", pendingIntent!!)
actions.add(remoteAction)
actionIntent = Intent(PlayerNotificationManager.ACTION_STOP)
pendingIntent =
PendingIntent.getBroadcast(applicationContext, REQUEST_CODE, actionIntent, 0)
icon = Icon.createWithResource(
applicationContext,
com.google.android.exoplayer2.ui.R.drawable.exo_notification_stop
)
remoteAction = RemoteAction(icon, "play", "stop the media", pendingIntent)
actions.add(remoteAction)
assert(videoPlayerFragment != null)
if (videoPlayerFragment!!.isPaused) {
Log.e(TAG, "setting actions with play button")
actionIntent = Intent(PlayerNotificationManager.ACTION_PLAY)
pendingIntent =
PendingIntent.getBroadcast(applicationContext, REQUEST_CODE, actionIntent, 0)
icon = Icon.createWithResource(
applicationContext,
com.google.android.exoplayer2.ui.R.drawable.exo_notification_play
)
remoteAction = RemoteAction(icon, "play", "play the media", pendingIntent)
} else {
Log.e(TAG, "setting actions with pause button")
actionIntent = Intent(PlayerNotificationManager.ACTION_PAUSE)
pendingIntent =
PendingIntent.getBroadcast(applicationContext, REQUEST_CODE, actionIntent, 0)
icon = Icon.createWithResource(
applicationContext,
com.google.android.exoplayer2.ui.R.drawable.exo_notification_pause
)
remoteAction = RemoteAction(icon, "pause", "pause the media", pendingIntent)
}
actions.add(remoteAction)
//add custom actions to pip window
val params = PictureInPictureParams.Builder()
.setActions(actions)
.build()
setPictureInPictureParams(params)
}
private fun changedToPipMode() {
val fragmentManager = supportFragmentManager
val videoPlayerFragment =
(fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
videoPlayerFragment.showControls(false)
//create custom actions
makePipControls()
//setup receiver to handle customer actions
val filter = IntentFilter()
filter.addAction(PlayerNotificationManager.ACTION_STOP)
filter.addAction(PlayerNotificationManager.ACTION_PAUSE)
filter.addAction(PlayerNotificationManager.ACTION_PLAY)
filter.addAction(getString(R.string.app_background_audio))
receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action!!
if (action == PlayerNotificationManager.ACTION_PAUSE) {
videoPlayerFragment.pauseVideo()
makePipControls()
}
if (action == PlayerNotificationManager.ACTION_PLAY) {
videoPlayerFragment.unPauseVideo()
makePipControls()
}
if (action == getString(R.string.app_background_audio)) {
unregisterReceiver(receiver)
finish()
}
if (action == PlayerNotificationManager.ACTION_STOP) {
unregisterReceiver(receiver)
finishAndRemoveTask()
}
}
}
registerReceiver(receiver, filter)
Log.v(TAG, "switched to pip ")
floatMode = true
videoPlayerFragment.showControls(false)
}
private fun changedToNormalMode() {
val fragmentManager = supportFragmentManager
val videoPlayerFragment =
(fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
videoPlayerFragment.showControls(true)
if (receiver != null) {
unregisterReceiver(receiver)
}
Log.v(TAG, "switched to normal")
floatMode = false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Set theme
val sharedPref = getSharedPreferences(packageName + "_preferences", Context.MODE_PRIVATE)
setTheme(
resources.getIdentifier(
sharedPref.getString(
getString(R.string.pref_theme_key),
getString(R.string.app_default_theme)
),
"style",
packageName
)
)
setContentView(R.layout.activity_video_play)
// get video ID
val intent = intent
val videoUuid = intent.getStringExtra(VideoListActivity.EXTRA_VIDEOID)
val videoPlayerFragment =
(supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
val playingVideo = videoPlayerFragment.videoUuid
Log.v(TAG, "oncreate click: $videoUuid is trying to replace: $playingVideo")
when {
TextUtils.isEmpty(playingVideo) -> {
Log.v(TAG, "oncreate no video currently playing")
videoPlayerFragment.start(videoUuid)
}
playingVideo != videoUuid -> {
Log.v(TAG, "oncreate different video playing currently")
videoPlayerFragment.stopVideo()
videoPlayerFragment.start(videoUuid)
}
else -> {
Log.v(TAG, "oncreate same video playing currently")
}
}
// if we are in landscape set the video to fullscreen
val orientation = this.resources.configuration.orientation
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
setOrientation(true)
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
val videoPlayerFragment =
(supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
val videoUuid = intent.getStringExtra(VideoListActivity.EXTRA_VIDEOID)
Log.v(
TAG,
"new intent click: " + videoUuid + " is trying to replace: " + videoPlayerFragment.videoUuid
)
val playingVideo = videoPlayerFragment.videoUuid
when {
TextUtils.isEmpty(playingVideo) -> {
Log.v(TAG, "new intent no video currently playing")
videoPlayerFragment.start(videoUuid)
}
playingVideo != videoUuid -> {
Log.v(TAG, "new intent different video playing currently")
videoPlayerFragment.stopVideo()
videoPlayerFragment.start(videoUuid)
}
else -> {
Log.v(TAG, "new intent same video playing currently")
}
}
// if we are in landscape set the video to fullscreen
val orientation = this.resources.configuration.orientation
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
setOrientation(true)
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
Log.v(TAG, "onConfigurationChanged()...")
super.onConfigurationChanged(newConfig)
// Checking the orientation changes of the screen
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
setOrientation(true)
} else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
setOrientation(false)
}
}
private fun setOrientation(isLandscape: Boolean) {
val fragmentManager = supportFragmentManager
val videoPlayerFragment =
fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?
val videoMetaFragment =
fragmentManager.findFragmentById(R.id.video_meta_data_fragment) as VideoMetaDataFragment?
assert(videoPlayerFragment != null)
val params = videoPlayerFragment!!.requireView().layoutParams as RelativeLayout.LayoutParams
params.width = FrameLayout.LayoutParams.MATCH_PARENT
params.height =
if (isLandscape) FrameLayout.LayoutParams.MATCH_PARENT else TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
250f,
resources.displayMetrics
)
.toInt()
videoPlayerFragment.requireView().layoutParams = params
if (videoMetaFragment != null) {
val transaction = fragmentManager.beginTransaction()
.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out)
if (isLandscape) {
transaction.hide(videoMetaFragment)
} else {
transaction.show(videoMetaFragment)
}
transaction.commit()
}
videoPlayerFragment.setIsFullscreen(isLandscape)
if (isLandscape) {
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
}
}
override fun onDestroy() {
val videoPlayerFragment =
(supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
videoPlayerFragment.destroyVideo()
super.onDestroy()
Log.v(TAG, "onDestroy...")
}
override fun onPause() {
super.onPause()
Log.v(TAG, "onPause()...")
}
override fun onResume() {
super.onResume()
Log.v(TAG, "onResume()...")
}
override fun onStop() {
super.onStop()
val videoPlayerFragment =
(supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
videoPlayerFragment.stopVideo()
// TODO: doesn't remove fragment??
val fragment: Fragment? = supportFragmentManager.findFragmentByTag(VideoDescriptionFragment.TAG)
if (fragment != null) {
Log.v(TAG, "remove VideoDescriptionFragment")
supportFragmentManager.beginTransaction().remove(fragment).commit()
}
Log.v(TAG, "onStop()...")
}
override fun onStart() {
super.onStart()
Log.v(TAG, "onStart()...")
}
@SuppressLint("NewApi")
public override fun onUserLeaveHint() {
Log.v(TAG, "onUserLeaveHint()...")
val sharedPref = getSharedPreferences(packageName + "_preferences", Context.MODE_PRIVATE)
val fragmentManager = supportFragmentManager
val videoPlayerFragment =
fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?
val videoMetaDataFragment =
fragmentManager.findFragmentById(R.id.video_meta_data_fragment) as VideoMetaDataFragment?
val backgroundBehavior = sharedPref.getString(
getString(R.string.pref_background_behavior_key),
getString(R.string.pref_background_stop_key)
)
assert(videoPlayerFragment != null)
assert(backgroundBehavior != null)
if (videoMetaDataFragment!!.isLeaveAppExpected) {
super.onUserLeaveHint()
return
}
if (backgroundBehavior == getString(R.string.pref_background_stop_key)) {
Log.v(TAG, "stop the video")
videoPlayerFragment!!.pauseVideo()
stopService(Intent(this, VideoPlayerService::class.java))
super.onBackPressed()
} else if (backgroundBehavior == getString(R.string.pref_background_audio_key)) {
Log.v(TAG, "play the Audio")
super.onBackPressed()
} else if (backgroundBehavior == getString(R.string.pref_background_float_key)) {
Log.v(TAG, "play in floating video")
//canEnterPIPMode makes sure API level is high enough
if (VideoHelper.canEnterPipMode(this)) {
Log.v(TAG, "enabling pip")
enterPipMode()
} else {
Log.v(TAG, "unable to use pip")
}
} else {
// Deal with bad entries from older version
Log.v(TAG, "No setting, fallback")
super.onBackPressed()
}
}
// @RequiresApi(api = Build.VERSION_CODES.O)
@SuppressLint("NewApi")
override fun onBackPressed() {
Log.v(TAG, "onBackPressed()...")
val sharedPref = getSharedPreferences(packageName + "_preferences", Context.MODE_PRIVATE)
val videoPlayerFragment =
(supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
// copying Youtube behavior to have back button exit full screen.
if (videoPlayerFragment.getIsFullscreen()) {
Log.v(TAG, "exiting full screen")
videoPlayerFragment.fullScreenToggle()
return
}
// pause video if pref is enabled
if (sharedPref.getBoolean(getString(R.string.pref_back_pause_key), true)) {
videoPlayerFragment.pauseVideo()
}
val backgroundBehavior = sharedPref.getString(
getString(R.string.pref_background_behavior_key),
getString(R.string.pref_background_stop_key)
)!!
if (backgroundBehavior == getString(R.string.pref_background_stop_key)) {
Log.v(TAG, "stop the video")
videoPlayerFragment.pauseVideo()
stopService(Intent(this, VideoPlayerService::class.java))
super.onBackPressed()
} else if (backgroundBehavior == getString(R.string.pref_background_audio_key)) {
Log.v(TAG, "play the Audio")
super.onBackPressed()
} else if (backgroundBehavior == getString(R.string.pref_background_float_key)) {
Log.v(TAG, "play in floating video")
//canEnterPIPMode makes sure API level is high enough
if (VideoHelper.canEnterPipMode(this)) {
Log.v(TAG, "enabling pip")
enterPipMode()
//fixes problem where back press doesn't bring up video list after returning from PIP mode
val intentSettings = Intent(this, VideoListActivity::class.java)
this.startActivity(intentSettings)
} else {
Log.v(TAG, "Unable to enter PIP mode")
super.onBackPressed()
}
} else {
// Deal with bad entries from older version
Log.v(TAG, "No setting, fallback")
super.onBackPressed()
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
fun enterPipMode() {
val fragmentManager = supportFragmentManager
val videoPlayerFragment =
fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?
if (videoPlayerFragment!!.videoAspectRatio == 0.toFloat()) {
Log.i(TAG, "impossible to switch to pip")
} else {
val rational = Rational((videoPlayerFragment.videoAspectRatio * 100).toInt(), 100)
val mParams = PictureInPictureParams.Builder()
.setAspectRatio(rational) // .setSourceRectHint(new Rect(0,500,400,600))
.build()
enterPictureInPictureMode(mParams)
}
}
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration
) {
val fragmentManager = supportFragmentManager
val videoPlayerFragment =
fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?
if (videoPlayerFragment != null) {
if (isInPictureInPictureMode) {
changedToPipMode()
Log.v(TAG, "switched to pip ")
videoPlayerFragment.useController(false)
} else {
changedToNormalMode()
Log.v(TAG, "switched to normal")
videoPlayerFragment.useController(true)
}
} else {
Log.e(TAG, "videoPlayerFragment is NULL")
}
}
companion object {
private const val TAG = "VideoPlayActivity"
var floatMode = false
private const val REQUEST_CODE = 101
}
}

View File

@ -4,19 +4,14 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import net.schueller.peertube.R
import net.schueller.peertube.databinding.ItemCategoryTitleBinding
import net.schueller.peertube.databinding.ItemChannelTitleBinding
import net.schueller.peertube.databinding.ItemTagTitleBinding
import net.schueller.peertube.databinding.RowVideoListBinding
import net.schueller.peertube.model.Category
import net.schueller.peertube.model.Channel
import net.schueller.peertube.model.TagVideo
import net.schueller.peertube.model.Video
import net.schueller.peertube.model.VideoList
import net.schueller.peertube.databinding.*
import net.schueller.peertube.fragment.VideoMetaDataFragment
import net.schueller.peertube.model.*
import net.schueller.peertube.model.ui.OverviewRecycleViewItem
import net.schueller.peertube.model.ui.VideoMetaViewItem
import java.util.ArrayList
class MultiViewRecycleViewAdapter : RecyclerView.Adapter<MultiViewRecyclerViewHolder>() {
class MultiViewRecycleViewAdapter(private val videoMetaDataFragment: VideoMetaDataFragment? = null) : RecyclerView.Adapter<MultiViewRecyclerViewHolder>() {
private var items = ArrayList<OverviewRecycleViewItem>()
set(value) {
@ -34,6 +29,11 @@ class MultiViewRecycleViewAdapter : RecyclerView.Adapter<MultiViewRecyclerViewHo
notifyDataSetChanged()
}
fun setVideoMeta(videoMetaViewItem: VideoMetaViewItem) {
items.add(videoMetaViewItem)
notifyDataSetChanged()
}
fun setCategoryTitle(category: Category) {
items.add(category)
notifyDataSetChanged()
@ -49,6 +49,11 @@ class MultiViewRecycleViewAdapter : RecyclerView.Adapter<MultiViewRecyclerViewHo
notifyDataSetChanged()
}
fun setVideoComment(commentThread: CommentThread) {
items.add(commentThread)
notifyDataSetChanged()
}
fun clearData() {
items.clear()
notifyDataSetChanged()
@ -83,6 +88,21 @@ class MultiViewRecycleViewAdapter : RecyclerView.Adapter<MultiViewRecyclerViewHo
false
)
)
R.layout.item_video_meta -> MultiViewRecyclerViewHolder.VideoMetaViewHolder(
ItemVideoMetaBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
),
videoMetaDataFragment
)
R.layout.item_video_comments_overview -> MultiViewRecyclerViewHolder.VideoCommentsViewHolder(
ItemVideoCommentsOverviewBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
else -> throw IllegalArgumentException("Invalid ViewType Provided")
}
}
@ -93,6 +113,8 @@ class MultiViewRecycleViewAdapter : RecyclerView.Adapter<MultiViewRecyclerViewHo
is MultiViewRecyclerViewHolder.CategoryViewHolder -> holder.bind(items[position] as Category)
is MultiViewRecyclerViewHolder.ChannelViewHolder -> holder.bind(items[position] as Channel)
is MultiViewRecyclerViewHolder.TagViewHolder -> holder.bind(items[position] as TagVideo)
is MultiViewRecyclerViewHolder.VideoMetaViewHolder -> holder.bind(items[position] as VideoMetaViewItem)
is MultiViewRecyclerViewHolder.VideoCommentsViewHolder -> holder.bind(items[position] as CommentThread)
}
}
@ -104,6 +126,8 @@ class MultiViewRecycleViewAdapter : RecyclerView.Adapter<MultiViewRecyclerViewHo
is Channel -> R.layout.item_channel_title
is Category -> R.layout.item_category_title
is TagVideo -> R.layout.item_tag_title
is VideoMetaViewItem -> R.layout.item_video_meta
is CommentThread -> R.layout.item_video_comments_overview
else -> { return 0}
}
}

View File

@ -16,47 +16,286 @@
*/
package net.schueller.peertube.adapter
import android.content.Context
import android.content.Intent
import android.util.Log
import android.view.MenuItem
import android.view.View
import android.view.View.GONE
import android.widget.Toast
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.google.gson.JsonObject
import com.squareup.picasso.Picasso
import net.schueller.peertube.R
import net.schueller.peertube.R.color
import net.schueller.peertube.R.string
import net.schueller.peertube.activity.AccountActivity
import net.schueller.peertube.activity.VideoListActivity
import net.schueller.peertube.activity.VideoListActivity.Companion
import net.schueller.peertube.activity.VideoPlayActivity
import net.schueller.peertube.databinding.ItemCategoryTitleBinding
import net.schueller.peertube.databinding.ItemChannelTitleBinding
import net.schueller.peertube.databinding.RowVideoListBinding
import net.schueller.peertube.helper.APIUrlHelper
import net.schueller.peertube.helper.MetaDataHelper.getDuration
import net.schueller.peertube.helper.MetaDataHelper.getMetaString
import net.schueller.peertube.helper.MetaDataHelper.getOwnerString
import net.schueller.peertube.model.Avatar
import net.schueller.peertube.model.Category
import net.schueller.peertube.model.Channel
import net.schueller.peertube.model.Video
import com.mikepenz.iconics.Iconics.Builder
import net.schueller.peertube.R.id
import net.schueller.peertube.R.menu
import net.schueller.peertube.databinding.ItemTagTitleBinding
import net.schueller.peertube.databinding.*
import net.schueller.peertube.fragment.VideoMetaDataFragment
import net.schueller.peertube.intents.Intents
import net.schueller.peertube.model.TagVideo
import net.schueller.peertube.model.*
import net.schueller.peertube.model.ui.VideoMetaViewItem
import net.schueller.peertube.network.GetVideoDataService
import net.schueller.peertube.network.RetrofitInstance
import net.schueller.peertube.network.Session
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import net.schueller.peertube.R
import net.schueller.peertube.network.GetUserService
sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.ViewHolder(binding.root) {
var videoRating: Rating? = null
var isLeaveAppExpected = false
class CategoryViewHolder(private val binding: ItemCategoryTitleBinding): MultiViewRecyclerViewHolder(binding) {
fun bind(category: Category) {
binding.textViewTitle.text = category.label
}
}
class VideoCommentsViewHolder(private val binding: ItemVideoCommentsOverviewBinding): MultiViewRecyclerViewHolder(binding) {
fun bind(commentThread: CommentThread) {
binding.videoCommentsTotalCount.text = commentThread.total.toString()
if (commentThread.comments.isNotEmpty()) {
val highlightedComment: Comment = commentThread.comments[0]
// owner / creator Avatar
val avatar = highlightedComment.account.avatar
if (avatar != null) {
val baseUrl = APIUrlHelper.getUrl(binding.videoHighlightedAvatar.context)
val avatarPath = avatar.path
Picasso.get()
.load(baseUrl + avatarPath)
.into(binding.videoHighlightedAvatar)
}
binding.videoHighlightedComment.text = highlightedComment.text
}
}
}
class VideoMetaViewHolder(private val binding: ItemVideoMetaBinding, private val videoMetaDataFragment: VideoMetaDataFragment?): MultiViewRecyclerViewHolder(binding) {
fun bind(videoMetaViewItem: VideoMetaViewItem) {
val video = videoMetaViewItem.video
if (video != null && videoMetaDataFragment != null) {
val context = binding.avatar.context
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
val videoDataService = RetrofitInstance.getRetrofitInstance(
apiBaseURL,
APIUrlHelper.useInsecureConnection(context)
).create(
GetVideoDataService::class.java
)
val userService = RetrofitInstance.getRetrofitInstance(
apiBaseURL,
APIUrlHelper.useInsecureConnection(context)
).create(
GetUserService::class.java
)
// Title
binding.videoName.text = video.name
binding.videoOpenDescription.setOnClickListener {
videoMetaDataFragment.showDescriptionFragment(video)
}
// Thumbs up
binding.videoThumbsUpWrapper.setOnClickListener {
rateVideo(true, video, context, binding)
}
// Thumbs Down
binding.videoThumbsDownWrapper.setOnClickListener {
rateVideo(false, video, context, binding)
}
binding.videoAddToPlaylistWrapper.setOnClickListener {
Toast.makeText(
context,
context.getString(string.video_feature_not_yet_implemented),
Toast.LENGTH_SHORT
).show()
}
binding.videoBlockWrapper.setOnClickListener {
Toast.makeText(
context,
context.getString(string.video_feature_not_yet_implemented),
Toast.LENGTH_SHORT
).show()
}
binding.videoFlagWrapper.setOnClickListener {
Toast.makeText(
context,
context.getString(string.video_feature_not_yet_implemented),
Toast.LENGTH_SHORT
).show()
}
// video rating
videoRating = Rating()
videoRating!!.rating = RATING_NONE // default
updateVideoRating(video, binding)
// Retrieve which rating the user gave to this video
if (Session.getInstance().isLoggedIn) {
val call = videoDataService.getVideoRating(video.id)
call.enqueue(object : Callback<Rating?> {
override fun onResponse(call: Call<Rating?>, response: Response<Rating?>) {
videoRating = response.body()
updateVideoRating(video, binding)
}
override fun onFailure(call: Call<Rating?>, t: Throwable) {
// Do nothing.
}
})
}
// Share
binding.videoShare.setOnClickListener {
isLeaveAppExpected = true
Intents.Share(context, video)
}
// hide download if not supported by video
if (video.downloadEnabled) {
binding.videoDownloadWrapper.setOnClickListener {
Intents.Download(context, video)
}
} else {
binding.videoDownloadWrapper.visibility = GONE
}
val account = video.account
// owner / creator Avatar
val avatar = account.avatar
if (avatar != null) {
val baseUrl = APIUrlHelper.getUrl(context)
val avatarPath = avatar.path
Picasso.get()
.load(baseUrl + avatarPath)
.into(binding.avatar)
}
// created at / views
binding.videoMeta.text = getMetaString(
video.createdAt,
video.views,
context!!
)
// owner / creator
binding.videoOwner.text = getOwnerString(
video.account.name,
video.account.host,
context
)
// videoOwnerSubscribers
binding.videoOwnerSubscribers.text = video.account.followersCount.toString()
// get subscription status
var isSubscribed = false
if (Session.getInstance().isLoggedIn) {
val subChannel = video.channel.name + "@" + video.channel.host
val call = userService.subscriptionsExist(subChannel)
call.enqueue(object : Callback<JsonObject> {
override fun onResponse(call: Call<JsonObject>, response: Response<JsonObject>) {
if (response.isSuccessful) {
// {"video.channel.name + "@" + video.channel.host":true}
if (response.body()?.get(video.channel.name + "@" + video.channel.host)!!.asBoolean) {
binding.videoOwnerSubscribeButton.setText(string.unsubscribe)
isSubscribed = true;
} else {
binding.videoOwnerSubscribeButton.setText(string.subscribe)
}
}
}
override fun onFailure(call: Call<JsonObject>, t: Throwable) {
// Do nothing.
}
})
}
binding.videoOwnerSubscribeButton.setOnClickListener {
if (Session.getInstance().isLoggedIn) {
if (!isSubscribed) {
val payload = video.channel.name + "@" + video.channel.host
val body = "{\"uri\":\"$payload\"}".toRequestBody("application/json".toMediaType())
val call = userService.subscribe(body)
call.enqueue(object : Callback<ResponseBody?> {
override fun onResponse(
call: Call<ResponseBody?>,
response: Response<ResponseBody?>
) {
if (response.isSuccessful) {
binding.videoOwnerSubscribeButton.setText(string.unsubscribe)
isSubscribed = true
}
}
override fun onFailure(call: Call<ResponseBody?>, t: Throwable) {
// Do nothing.
}
})
} else {
val payload = video.channel.name + "@" + video.channel.host
val call = userService.unsubscribe(payload)
call.enqueue(object : Callback<ResponseBody?> {
override fun onResponse(
call: Call<ResponseBody?>,
response: Response<ResponseBody?>
) {
if (response.isSuccessful) {
binding.videoOwnerSubscribeButton.setText(string.subscribe)
isSubscribed = false
}
}
override fun onFailure(call: Call<ResponseBody?>, t: Throwable) {
// Do nothing.
}
})
}
} else {
Toast.makeText(
context,
context.getString(string.video_login_required_for_service),
Toast.LENGTH_SHORT
).show()
}
}
}
}
}
class ChannelViewHolder(private val binding: ItemChannelTitleBinding): MultiViewRecyclerViewHolder(binding) {
fun bind(channel: Channel) {
@ -178,4 +417,117 @@ sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.Vi
}
fun updateVideoRating(video: Video?, binding: ItemVideoMetaBinding) {
when (videoRating!!.rating) {
RATING_NONE -> {
Log.v("MWCVH", "RATING_NONE")
binding.videoThumbsUp.setImageResource(R.drawable.ic_thumbs_up)
binding.videoThumbsDown.setImageResource(R.drawable.ic_thumbs_down)
}
RATING_LIKE -> {
Log.v("MWCVH", "RATING_LIKE")
binding.videoThumbsUp.setImageResource(R.drawable.ic_thumbs_up_filled)
binding.videoThumbsDown.setImageResource(R.drawable.ic_thumbs_down)
}
RATING_DISLIKE -> {
Log.v("MWCVH", "RATING_DISLIKE")
binding.videoThumbsUp.setImageResource(R.drawable.ic_thumbs_up)
binding.videoThumbsDown.setImageResource(R.drawable.ic_thumbs_down_filled)
}
}
// Update the texts
binding.videoThumbsUpTotal.text = video?.likes.toString()
binding.videoThumbsDownTotal.text = video?.dislikes.toString()
}
/**
* TODO: move this out and get update when rating changes
*/
fun rateVideo(like: Boolean, video: Video, context: Context, binding: ItemVideoMetaBinding) {
if (Session.getInstance().isLoggedIn) {
val ratePayload: String = when (videoRating!!.rating) {
RATING_LIKE -> if (like) RATING_NONE else RATING_DISLIKE
RATING_DISLIKE -> if (like) RATING_LIKE else RATING_NONE
RATING_NONE -> if (like) RATING_LIKE else RATING_DISLIKE
else -> if (like) RATING_LIKE else RATING_DISLIKE
}
val body = "{\"rating\":\"$ratePayload\"}".toRequestBody("application/json".toMediaType())
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
val videoDataService = RetrofitInstance.getRetrofitInstance(
apiBaseURL, APIUrlHelper.useInsecureConnection(
context
)
).create(
GetVideoDataService::class.java
)
val call = videoDataService.rateVideo(video.id, body)
call.enqueue(object : Callback<ResponseBody?> {
override fun onResponse(
call: Call<ResponseBody?>,
response: Response<ResponseBody?>
) {
// if 20x, update likes/dislikes
if (response.isSuccessful) {
val previousRating = videoRating!!.rating
// Update the likes/dislikes count of the video, if needed.
// This is only a visual trick, as the actual like/dislike count has
// already been modified on the PeerTube instance.
if (previousRating != ratePayload) {
when (previousRating) {
RATING_NONE -> if (ratePayload == RATING_LIKE) {
video.likes = video.likes + 1
} else {
video.dislikes = video.dislikes + 1
}
RATING_LIKE -> {
video.likes = video.likes - 1
if (ratePayload == RATING_DISLIKE) {
video.dislikes = video.dislikes + 1
}
}
RATING_DISLIKE -> {
video.dislikes = video.dislikes - 1
if (ratePayload == RATING_LIKE) {
video.likes = video.likes + 1
}
}
}
}
videoRating!!.rating = ratePayload
updateVideoRating(video, binding)
}
}
override fun onFailure(call: Call<ResponseBody?>, t: Throwable) {
Toast.makeText(
context,
context.getString(string.video_rating_failed),
Toast.LENGTH_SHORT
).show()
}
})
} else {
Toast.makeText(
context,
context.getString(string.video_login_required_for_service),
Toast.LENGTH_SHORT
).show()
}
}
companion object {
private const val RATING_NONE = "none"
private const val RATING_LIKE = "like"
private const val RATING_DISLIKE = "dislike"
const val EXTRA_VIDEOID = "VIDEOID"
const val EXTRA_ACCOUNTDISPLAYNAME = "ACCOUNTDISPLAYNAMEANDHOST"
}
}

View File

@ -20,7 +20,7 @@ import android.os.Parcelable
import androidx.room.PrimaryKey
import androidx.room.ColumnInfo
import androidx.room.Entity
import kotlinx.android.parcel.Parcelize
import kotlinx.parcelize.Parcelize
@Parcelize
@Entity(tableName = "server_table")

View File

@ -17,7 +17,6 @@
package net.schueller.peertube.database
import android.app.Application
import android.os.AsyncTask
import androidx.lifecycle.LiveData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

View File

@ -19,12 +19,13 @@ package net.schueller.peertube.fragment
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.util.Patterns
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import net.schueller.peertube.R
@ -52,7 +53,7 @@ class AddServerFragment : Fragment() {
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
mBinding = FragmentAddServerBinding.inflate(inflater, container, false)
return mBinding.root
}
@ -115,7 +116,7 @@ class AddServerFragment : Fragment() {
mBinding.pickServerUrl.setOnClickListener {
val intentServer = Intent(activity, SearchServerActivity::class.java)
this.startActivityForResult(intentServer, PICK_SERVER)
openActivityForResult(intentServer)
}
}
@ -132,35 +133,24 @@ class AddServerFragment : Fragment() {
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode != PICK_SERVER) {
return
}
if (resultCode != Activity.RESULT_OK) {
return
}
val serverUrlTest = data?.getStringExtra("serverUrl")
//Log.d(TAG, "serverUrl " + serverUrlTest);
mBinding.serverUrl.setText(serverUrlTest)
mBinding.serverLabel.apply {
if (text.toString().isBlank()) {
setText(data?.getStringExtra("serverName"))
private var resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
val intent = result.data
val serverUrlTest = intent?.getStringExtra("serverUrl")
mBinding.serverUrl.setText(serverUrlTest)
mBinding.serverLabel.apply {
if (text.toString().isBlank()) {
setText(intent?.getStringExtra("serverName"))
}
}
}
}
private fun openActivityForResult(intent: Intent) {
resultLauncher.launch(intent)
}
companion object {
private const val TAG = "AddServerFragment"
private const val PICK_SERVER = 1
private const val SERVER_ARG = "server"
fun newInstance(server: Server) = AddServerFragment().apply {

View File

@ -0,0 +1,120 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.fragment
import android.view.LayoutInflater
import android.view.ViewGroup
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.ImageButton
import net.schueller.peertube.R
import android.widget.TextView
import androidx.fragment.app.Fragment
import net.schueller.peertube.helper.APIUrlHelper
import net.schueller.peertube.helper.ErrorHelper
import net.schueller.peertube.model.Description
import net.schueller.peertube.model.Video
import net.schueller.peertube.network.GetVideoDataService
import net.schueller.peertube.network.RetrofitInstance
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class VideoDescriptionFragment : Fragment () {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(
R.layout.fragment_video_description, container,
false
)
val video = video
if (video != null) {
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
val videoDataService = RetrofitInstance.getRetrofitInstance(
apiBaseURL,
APIUrlHelper.useInsecureConnection(context)
).create(
GetVideoDataService::class.java
)
// description, get extended if available
val videoDescription = view.findViewById<TextView>(R.id.description)
val shortDescription = video.description
if (shortDescription != null && shortDescription.length > 237) {
val call = videoDataService.getVideoFullDescription(video.uuid);
call.enqueue(object : Callback<Description?> {
override fun onResponse(call: Call<Description?>, response: Response<Description?>) {
val videoFullDescription: Description? = response.body();
videoDescription.text = videoFullDescription?.description
}
override fun onFailure(call: Call<Description?>, t: Throwable) {
Log.wtf(TAG, t.fillInStackTrace())
ErrorHelper.showToastFromCommunicationError(activity, t)
}
})
}
videoDescription.text = shortDescription;
val closeButton = view.findViewById<ImageButton>(R.id.video_description_close_button)
closeButton.setOnClickListener {
videoMetaDataFragment!!.hideDescriptionFragment()
}
// video privacy
val videoPrivacy = view.findViewById<TextView>(R.id.video_privacy);
videoPrivacy.text = video!!.privacy.label;
// video category
val videoCategory = view.findViewById<TextView>(R.id.video_category);
videoCategory.text = video!!.category.label;
// video privacy
val videoLicense = view.findViewById<TextView>(R.id.video_license);
videoLicense.text = video!!.licence.label;
// video language
val videoLanguage = view.findViewById<TextView>(R.id.video_language);
videoLanguage.text = video!!.language.label;
// video privacy
val videoTags = view.findViewById<TextView>(R.id.video_tags);
videoTags.text = android.text.TextUtils.join(", ", video!!.tags);
}
return view
}
companion object {
private var video: Video? = null
private var videoMetaDataFragment: VideoMetaDataFragment? = null
const val TAG = "VideoDescr"
fun newInstance(mVideo: Video?, mVideoMetaDataFragment: VideoMetaDataFragment): VideoDescriptionFragment {
video = mVideo
videoMetaDataFragment = mVideoMetaDataFragment
return VideoDescriptionFragment()
}
}
}

View File

@ -1,408 +0,0 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.fragment;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.os.Bundle;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.mikepenz.iconics.Iconics;
import com.squareup.picasso.Picasso;
import java.util.Objects;
import net.schueller.peertube.R;
import net.schueller.peertube.helper.APIUrlHelper;
import net.schueller.peertube.helper.ErrorHelper;
import net.schueller.peertube.helper.MetaDataHelper;
import net.schueller.peertube.intents.Intents;
import net.schueller.peertube.model.Account;
import net.schueller.peertube.model.Avatar;
import net.schueller.peertube.model.Description;
import net.schueller.peertube.model.Rating;
import net.schueller.peertube.model.Video;
import net.schueller.peertube.network.GetVideoDataService;
import net.schueller.peertube.network.RetrofitInstance;
import net.schueller.peertube.network.Session;
import net.schueller.peertube.service.VideoPlayerService;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.PopupMenu;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.Fragment;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class VideoMetaDataFragment extends Fragment {
private static final String TAG = "VideoMetaDataFragment";
private static final String RATING_NONE = "none";
private static final String RATING_LIKE = "like";
private static final String RATING_DISLIKE = "dislike";
private Rating videoRating;
private ColorStateList defaultTextColor;
private boolean leaveAppExpected = false;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_video_meta, container, false);
}
@Override
public void onPause()
{
leaveAppExpected = false;
super.onPause();
}
public boolean isLeaveAppExpected()
{
return leaveAppExpected;
}
public void updateVideoMeta(Video video, VideoPlayerService mService) {
Context context = getContext();
Activity activity = getActivity();
String apiBaseURL = APIUrlHelper.getUrlWithVersion(context);
GetVideoDataService videoDataService = RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(context)).create(GetVideoDataService.class);
// Thumbs up
Button thumbsUpButton = activity.findViewById(R.id.video_thumbs_up);
defaultTextColor = thumbsUpButton.getTextColors();
thumbsUpButton.setText(R.string.video_thumbs_up_icon);
new Iconics.Builder().on(thumbsUpButton).build();
thumbsUpButton.setOnClickListener(v -> {
rateVideo(true, video);
});
// Thumbs Down
Button thumbsDownButton = activity.findViewById(R.id.video_thumbs_down);
thumbsDownButton.setText(R.string.video_thumbs_down_icon);
new Iconics.Builder().on(thumbsDownButton).build();
thumbsDownButton.setOnClickListener(v -> {
rateVideo(false, video);
});
// video rating
videoRating = new Rating();
videoRating.setRating(RATING_NONE); // default
updateVideoRating(video);
// Retrieve which rating the user gave to this video
if (Session.getInstance().isLoggedIn()) {
Call<Rating> call = videoDataService.getVideoRating(video.getId());
call.enqueue(new Callback<Rating>() {
@Override
public void onResponse(Call<Rating> call, Response<Rating> response) {
videoRating = response.body();
updateVideoRating(video);
}
@Override
public void onFailure(Call<Rating> call, Throwable t) {
ErrorHelper.showToastFromCommunicationError( getActivity(), t );
// Do nothing.
}
});
}
// Share
Button videoShareButton = activity.findViewById(R.id.video_share);
videoShareButton.setText(R.string.video_share_icon);
new Iconics.Builder().on(videoShareButton).build();
videoShareButton.setOnClickListener(v ->
{
leaveAppExpected = true;
Intents.Share( context, video );
} );
// Download
Button videoDownloadButton = activity.findViewById(R.id.video_download);
videoDownloadButton.setText(R.string.video_download_icon);
new Iconics.Builder().on(videoDownloadButton).build();
videoDownloadButton.setOnClickListener(v -> {
// get permission to store file
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
leaveAppExpected = true;
ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
Intents.Download(context, video);
} else {
Toast.makeText(context, getString(R.string.video_download_permission_error), Toast.LENGTH_LONG).show();
}
} else {
Intents.Download(context, video);
}
});
Account account = video.getAccount();
// owner / creator Avatar
Avatar avatar = account.getAvatar();
if (avatar != null) {
ImageView avatarView = activity.findViewById(R.id.avatar);
String baseUrl = APIUrlHelper.getUrl(context);
String avatarPath = avatar.getPath();
Picasso.get()
.load(baseUrl + avatarPath)
.into(avatarView);
}
// title / name
TextView videoName = activity.findViewById(R.id.sl_row_name);
videoName.setText(video.getName());
// created at / views
TextView videoMeta = activity.findViewById(R.id.videoMeta);
videoMeta.setText(
MetaDataHelper.getMetaString(
video.getCreatedAt(),
video.getViews(),
context
)
);
// owner / creator
TextView videoOwner = activity.findViewById(R.id.videoOwner);
videoOwner.setText(
MetaDataHelper.getOwnerString(video.getAccount().getName(),
video.getAccount().getHost(),
context
)
);
// description
TextView videoDescription = activity.findViewById(R.id.description);
String shortDescription = video.getDescription();
if (shortDescription != null && Objects.requireNonNull(shortDescription).length() > 237) {
shortDescription += "\n" + getString(R.string.video_description_read_more);
videoDescription.setOnClickListener(v -> {
Call<Description> call = videoDataService.getVideoFullDescription(video.getUuid());
call.enqueue(new Callback<Description>() {
@Override
public void onResponse(Call<Description> call, Response<Description> response) {
if (response.isSuccessful() && response.body() != null) {
new Description();
Description videoFullDescription;
videoFullDescription = response.body();
videoDescription.setText(videoFullDescription.getDescription());
}
}
@Override
public void onFailure(Call<Description> call, Throwable t) {
Toast.makeText(getContext(), getString(R.string.video_get_full_description_failed), Toast.LENGTH_SHORT).show();
}
});
});
}
videoDescription.setText(shortDescription);
// video privacy
TextView videoPrivacy = activity.findViewById(R.id.video_privacy);
videoPrivacy.setText(video.getPrivacy().getLabel());
// video category
TextView videoCategory = activity.findViewById(R.id.video_category);
videoCategory.setText(video.getCategory().getLabel());
// video privacy
TextView videoLicense = activity.findViewById(R.id.video_license);
videoLicense.setText(video.getLicence().getLabel());
// video language
TextView videoLanguage = activity.findViewById(R.id.video_language);
videoLanguage.setText(video.getLanguage().getLabel());
// video privacy
TextView videoTags = activity.findViewById(R.id.video_tags);
videoTags.setText(android.text.TextUtils.join(", ", video.getTags()));
// more button
TextView moreButton = activity.findViewById(R.id.moreButton);
moreButton.setText(R.string.video_more_icon);
new Iconics.Builder().on(moreButton).build();
moreButton.setOnClickListener(v -> {
PopupMenu popup = new PopupMenu(context, v);
popup.setOnMenuItemClickListener(menuItem -> {
switch (menuItem.getItemId()) {
case R.id.video_more_report:
Log.v(TAG, "Report");
Toast.makeText(context, "Not Implemented", Toast.LENGTH_SHORT).show();
return true;
case R.id.video_more_blacklist:
Log.v(TAG, "Blacklist");
Toast.makeText(context, "Not Implemented", Toast.LENGTH_SHORT).show();
return true;
default:
return false;
}
});
popup.inflate(R.menu.menu_video_more);
popup.show();
});
// video player options
TextView videoOptions = activity.findViewById(R.id.exo_more);
videoOptions.setText(R.string.video_more_icon);
new Iconics.Builder().on(videoOptions).build();
videoOptions.setOnClickListener(v -> {
VideoOptionsFragment videoOptionsFragment =
VideoOptionsFragment.newInstance(mService, video.getFiles());
videoOptionsFragment.show(getActivity().getSupportFragmentManager(),
VideoOptionsFragment.TAG);
});
}
void updateVideoRating(Video video) {
Button thumbsUpButton = getActivity().findViewById(R.id.video_thumbs_up);
Button thumbsDownButton = getActivity().findViewById(R.id.video_thumbs_down);
TypedValue typedValue = new TypedValue();
TypedArray a = getContext().obtainStyledAttributes(typedValue.data, new int[]{R.attr.colorPrimary});
int accentColor = a.getColor(0, 0);
// Change the color of the thumbs
switch (videoRating.getRating()) {
case RATING_NONE:
thumbsUpButton.setTextColor(defaultTextColor);
thumbsDownButton.setTextColor(defaultTextColor);
break;
case RATING_LIKE:
thumbsUpButton.setTextColor(accentColor);
thumbsDownButton.setTextColor(defaultTextColor);
break;
case RATING_DISLIKE:
thumbsUpButton.setTextColor(defaultTextColor);
thumbsDownButton.setTextColor(accentColor);
break;
}
// Update the texts
TextView thumbsDownTotal = getActivity().findViewById(R.id.video_thumbs_down_total);
TextView thumbsUpTotal = getActivity().findViewById(R.id.video_thumbs_up_total);
thumbsUpTotal.setText(String.valueOf(video.getLikes()));
thumbsDownTotal.setText(String.valueOf(video.getDislikes()));
a.recycle();
}
void rateVideo(Boolean like, Video video) {
if (Session.getInstance().isLoggedIn()) {
final String ratePayload;
switch (videoRating.getRating()) {
case RATING_LIKE:
ratePayload = like ? RATING_NONE : RATING_DISLIKE;
break;
case RATING_DISLIKE:
ratePayload = like ? RATING_LIKE : RATING_NONE;
break;
case RATING_NONE:
default:
ratePayload = like ? RATING_LIKE : RATING_DISLIKE;
break;
}
RequestBody body = RequestBody.create(okhttp3.MediaType.parse("application/json"), "{\"rating\":\"" + ratePayload + "\"}");
String apiBaseURL = APIUrlHelper.getUrlWithVersion(getContext());
GetVideoDataService videoDataService = RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(getContext())).create(GetVideoDataService.class);
Call<ResponseBody> call = videoDataService.rateVideo(video.getId(), body);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
//Log.v(TAG, response.toString());
// if 20x, update likes/dislikes
if (response.isSuccessful()) {
String previousRating = videoRating.getRating();
// Update the likes/dislikes count of the video, if needed.
// This is only a visual trick, as the actual like/dislike count has
// already been modified on the PeerTube instance.
if (!previousRating.equals(ratePayload)) {
switch (previousRating) {
case RATING_NONE:
if (ratePayload.equals(RATING_LIKE)) {
video.setLikes(video.getLikes() + 1);
} else {
video.setDislikes(video.getDislikes() + 1);
}
break;
case RATING_LIKE:
video.setLikes(video.getLikes() - 1);
if (ratePayload.equals(RATING_DISLIKE)) {
video.setDislikes(video.getDislikes() + 1);
}
break;
case RATING_DISLIKE:
video.setDislikes(video.getDislikes() - 1);
if (ratePayload.equals(RATING_LIKE)) {
video.setLikes(video.getLikes() + 1);
}
break;
}
}
videoRating.setRating(ratePayload);
updateVideoRating(video);
}
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
Toast.makeText(getContext(), getString(R.string.video_rating_failed), Toast.LENGTH_SHORT).show();
}
});
} else {
Toast.makeText(getContext(), getString(R.string.video_login_required_for_service), Toast.LENGTH_SHORT).show();
}
}
}

View File

@ -0,0 +1,240 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.fragment
import android.Manifest
import net.schueller.peertube.helper.MetaDataHelper.getMetaString
import net.schueller.peertube.helper.MetaDataHelper.getOwnerString
import android.content.res.ColorStateList
import android.view.LayoutInflater
import android.view.ViewGroup
import android.os.Bundle
import net.schueller.peertube.R
import net.schueller.peertube.service.VideoPlayerService
import android.app.Activity
import android.content.Context
import net.schueller.peertube.helper.APIUrlHelper
import net.schueller.peertube.network.GetVideoDataService
import net.schueller.peertube.network.RetrofitInstance
import net.schueller.peertube.helper.ErrorHelper
import androidx.core.app.ActivityCompat
import android.content.pm.PackageManager
import android.util.Log
import android.widget.Toast
import com.squareup.picasso.Picasso
import android.widget.TextView
import android.util.TypedValue
import android.view.View
import android.widget.Button
import android.widget.ImageView
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.mikepenz.iconics.Iconics
import net.schueller.peertube.adapter.MultiViewRecycleViewAdapter
import net.schueller.peertube.intents.Intents
import net.schueller.peertube.model.CommentThread
import net.schueller.peertube.model.Rating
import net.schueller.peertube.model.Video
import net.schueller.peertube.model.VideoList
import net.schueller.peertube.model.ui.VideoMetaViewItem
import net.schueller.peertube.network.GetUserService
import net.schueller.peertube.network.Session
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.lang.Exception
class VideoMetaDataFragment : Fragment() {
private var videoRating: Rating? = null
private var defaultTextColor: ColorStateList? = null
private var recyclerView: RecyclerView? = null
private var mMultiViewAdapter: MultiViewRecycleViewAdapter? = null
private lateinit var videoDescriptionFragment: VideoDescriptionFragment
var isLeaveAppExpected = false
private set
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_video_meta, container, false)
}
override fun onPause() {
isLeaveAppExpected = false
super.onPause()
}
fun showDescriptionFragment(video: Video) {
// show full description fragment
videoDescriptionFragment = VideoDescriptionFragment.newInstance(video, this)
childFragmentManager.beginTransaction()
.add(R.id.video_meta_data_fragment, videoDescriptionFragment, VideoDescriptionFragment.TAG).commit()
}
fun hideDescriptionFragment() {
val fragment: Fragment? = childFragmentManager.findFragmentByTag(VideoDescriptionFragment.TAG)
if (fragment != null) {
childFragmentManager.beginTransaction().remove(fragment).commit()
}
}
fun updateVideoMeta(video: Video, mService: VideoPlayerService?) {
// Remove description if it is open as we are loading a new video
hideDescriptionFragment()
val context = context
val activity: Activity? = activity
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
val videoDataService = RetrofitInstance.getRetrofitInstance(
apiBaseURL,
APIUrlHelper.useInsecureConnection(context)
).create(
GetVideoDataService::class.java
)
// related videos
recyclerView = activity!!.findViewById(R.id.relatedVideosView)
val layoutManager: RecyclerView.LayoutManager = LinearLayoutManager(this@VideoMetaDataFragment.context)
recyclerView?.layoutManager = layoutManager
mMultiViewAdapter = MultiViewRecycleViewAdapter(this)
recyclerView?.adapter = mMultiViewAdapter
val videoMetaViewItem = VideoMetaViewItem()
videoMetaViewItem.video = video
mMultiViewAdapter?.setVideoMeta(videoMetaViewItem)
loadVideos()
// loadComments(video.id)
// mMultiViewAdapter?.setVideoComment()
// videoOwnerSubscribeButton
// description
// video player options
val videoOptions = activity.findViewById<TextView>(R.id.exo_more)
videoOptions.setText(R.string.video_more_icon)
Iconics.Builder().on(videoOptions).build()
videoOptions.setOnClickListener {
val videoOptionsFragment = VideoOptionsFragment.newInstance(mService, video.files)
videoOptionsFragment.show(
getActivity()!!.supportFragmentManager,
VideoOptionsFragment.TAG
)
}
}
private fun loadComments(videoId: Int) {
val context = context
val start = 0
val count = 1
val sort = "-createdAt"
// We set this to default to null so that on initial start there are videos listed.
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
val service =
RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(context)).create(
GetVideoDataService::class.java
)
val call: Call<CommentThread> = service.getCommentThreads(videoId, start, count, sort)
call.enqueue(object : Callback<CommentThread?> {
override fun onResponse(call: Call<CommentThread?>, response: Response<CommentThread?>) {
if (response.body() != null) {
val commentThread = response.body()
if (commentThread != null) {
mMultiViewAdapter!!.setVideoComment(commentThread);
}
}
}
override fun onFailure(call: Call<CommentThread?>, t: Throwable) {
Log.wtf("err", t.fillInStackTrace())
ErrorHelper.showToastFromCommunicationError(this@VideoMetaDataFragment.context, t)
}
})
}
private fun loadVideos() {
val context = context
val start = 0
val count = 6
val sort = "-createdAt"
val filter: String? = null
val sharedPref = context?.getSharedPreferences(
context.packageName + "_preferences",
Context.MODE_PRIVATE
)
var nsfw = "false"
var languages: Set<String>? = emptySet()
if (sharedPref != null) {
nsfw = if (sharedPref.getBoolean(getString(R.string.pref_show_nsfw_key), false)) "both" else "false"
languages = sharedPref.getStringSet(getString(R.string.pref_video_language_key), null)
}
// We set this to default to null so that on initial start there are videos listed.
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
val service =
RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(context)).create(
GetVideoDataService::class.java
)
val call: Call<VideoList> = service.getVideosData(start, count, sort, nsfw, filter, languages)
/*Log the URL called*/Log.d("URL Called", call.request().url.toString() + "")
// Toast.makeText(VideoListActivity.this, "URL Called: " + call.request().url(), Toast.LENGTH_SHORT).show();
call.enqueue(object : Callback<VideoList?> {
override fun onResponse(call: Call<VideoList?>, response: Response<VideoList?>) {
if (response.body() != null) {
val videoList = response.body()
if (videoList != null) {
mMultiViewAdapter!!.setVideoListData(videoList)
}
}
}
override fun onFailure(call: Call<VideoList?>, t: Throwable) {
Log.wtf("err", t.fillInStackTrace())
ErrorHelper.showToastFromCommunicationError(this@VideoMetaDataFragment.context, t)
}
})
}
companion object {
const val TAG = "VMDF"
}
}

View File

@ -1,516 +0,0 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.fragment;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.github.se_bastiaan.torrentstream.StreamStatus;
import com.github.se_bastiaan.torrentstream.Torrent;
import com.github.se_bastiaan.torrentstream.TorrentOptions;
import com.github.se_bastiaan.torrentstream.TorrentStream;
import com.github.se_bastiaan.torrentstream.listeners.TorrentListener;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
import com.mikepenz.iconics.Iconics;
import net.schueller.peertube.R;
import net.schueller.peertube.helper.APIUrlHelper;
import net.schueller.peertube.helper.ErrorHelper;
import net.schueller.peertube.model.File;
import net.schueller.peertube.model.Video;
import net.schueller.peertube.network.GetVideoDataService;
import net.schueller.peertube.network.RetrofitInstance;
import net.schueller.peertube.service.VideoPlayerService;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.fragment.app.Fragment;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import static net.schueller.peertube.helper.VideoHelper.canEnterPipMode;
public class VideoPlayerFragment extends Fragment implements VideoRendererEventListener {
private String mVideoUuid;
private ProgressBar progressBar;
private PlayerView simpleExoPlayerView;
private Intent videoPlayerIntent;
private Boolean mBound = false;
private Boolean isFullscreen = false;
private VideoPlayerService mService;
private TorrentStream torrentStream;
private LinearLayout torrentStatus;
private float aspectRatio;
private static final String TAG = "VideoPlayerFragment";
private GestureDetector mDetector;
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
Log.d(TAG, "onServiceConnected");
VideoPlayerService.LocalBinder binder = (VideoPlayerService.LocalBinder) service;
mService = binder.getService();
// 2. Create the player
simpleExoPlayerView.setPlayer(mService.player);
mBound = true;
loadVideo();
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
Log.d(TAG, "onServiceDisconnected");
simpleExoPlayerView.setPlayer(null);
mBound = false;
}
};
private AspectRatioFrameLayout.AspectRatioListener aspectRatioListerner = new AspectRatioFrameLayout.AspectRatioListener()
{
@Override
public void onAspectRatioUpdated( float targetAspectRatio, float naturalAspectRatio, boolean aspectRatioMismatch )
{
aspectRatio = targetAspectRatio;
}
};
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_video_player, container, false);
}
public void start(String videoUuid) {
// start service
Context context = getContext();
Activity activity = getActivity();
mVideoUuid = videoUuid;
assert activity != null;
progressBar = activity.findViewById(R.id.torrent_progress);
progressBar.setMax(100);
assert context != null;
simpleExoPlayerView = new PlayerView(context);
simpleExoPlayerView = activity.findViewById(R.id.video_view);
simpleExoPlayerView.setControllerShowTimeoutMs(1000);
simpleExoPlayerView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT);
mDetector = new GestureDetector(context, new MyGestureListener());
simpleExoPlayerView.setOnTouchListener(touchListener);
simpleExoPlayerView.setAspectRatioListener( aspectRatioListerner );
torrentStatus = activity.findViewById(R.id.exo_torrent_status);
// Full screen Icon
TextView fullscreenText = activity.findViewById(R.id.exo_fullscreen);
FrameLayout fullscreenButton = activity.findViewById(R.id.exo_fullscreen_button);
fullscreenText.setText(R.string.video_expand_icon);
new Iconics.Builder().on(fullscreenText).build();
fullscreenButton.setOnClickListener(view -> {
Log.d(TAG, "Fullscreen");
fullScreenToggle();
});
if (!mBound) {
videoPlayerIntent = new Intent(context, VideoPlayerService.class);
activity.bindService(videoPlayerIntent, mConnection, Context.BIND_AUTO_CREATE);
}
}
private void loadVideo() {
Context context = getContext();
// get video details from api
String apiBaseURL = APIUrlHelper.getUrlWithVersion(context);
GetVideoDataService service = RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(context)).create(GetVideoDataService.class);
Call<Video> call = service.getVideoData(mVideoUuid);
call.enqueue(new Callback<Video>() {
@Override
public void onResponse(@NonNull Call<Video> call, @NonNull Response<Video> response) {
Video video = response.body();
mService.setCurrentVideo(video);
if (video == null) {
Toast.makeText(context, "Unable to retrieve video information, try again later.", Toast.LENGTH_SHORT).show();
return;
}
playVideo(video);
}
@Override
public void onFailure(@NonNull Call<Video> call, @NonNull Throwable t) {
Log.wtf(TAG, t.fillInStackTrace());
ErrorHelper.showToastFromCommunicationError( getActivity(), t );
}
});
}
public void useController(boolean value) {
if (mBound) {
simpleExoPlayerView.setUseController(value);
}
}
private void playVideo(Video video) {
Context context = getContext();
// video Meta fragment
VideoMetaDataFragment videoMetaDataFragment = (VideoMetaDataFragment)
requireActivity().getSupportFragmentManager().findFragmentById(R.id.video_meta_data_fragment);
assert videoMetaDataFragment != null;
videoMetaDataFragment.updateVideoMeta(video, mService);
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
if (sharedPref.getBoolean(getString(R.string.pref_torrent_player_key), false)) {
torrentStatus.setVisibility(View.VISIBLE);
String stream = video.getFiles().get(0).getTorrentUrl();
Log.v(TAG, "getTorrentUrl : " + video.getFiles().get(0).getTorrentUrl());
torrentStream = setupTorrentStream();
torrentStream.startStream(stream);
} else {
Integer videoQuality = sharedPref.getInt(getString(R.string.pref_quality_key), 999999);
String urlToPlay = null;
boolean isHLS = false;
// try HLS stream first
// get video qualities
// TODO: if auto is set all versions except 0p should be added to a track and have exoplayer auto select optimal bitrate
if (video.getStreamingPlaylists().size() > 0) {
urlToPlay = video.getStreamingPlaylists().get( 0 ).getPlaylistUrl();
isHLS = true;
} else {
if (video.getFiles().size() > 0) {
urlToPlay = video.getFiles().get( 0 ).getFileUrl(); // default, take first found, usually highest res
for ( File file : video.getFiles() ) {
// Set quality if it matches
if ( file.getResolution().getId().equals( videoQuality ) ) {
urlToPlay = file.getFileUrl();
}
}
}
}
if (!urlToPlay.isEmpty()) {
mService.setCurrentStreamUrl( urlToPlay, isHLS);
torrentStatus.setVisibility(View.GONE);
startPlayer();
} else {
stopVideo();
Toast.makeText(context, R.string.api_error, Toast.LENGTH_LONG).show();
}
}
Log.v(TAG, "end of load Video");
}
private void startPlayer() {
Util.startForegroundService(requireContext(), videoPlayerIntent);
}
public void destroyVideo() {
simpleExoPlayerView.setPlayer(null);
if (torrentStream != null) {
torrentStream.stopStream();
}
}
public void pauseVideo() {
if (mBound) {
mService.player.setPlayWhenReady(false);
}
}
public void pauseToggle() {
if (mBound) {
mService.player.setPlayWhenReady(!mService.player.getPlayWhenReady());
}
}
public void unPauseVideo() {
if (mBound) {
mService.player.setPlayWhenReady(true);
}
}
public float getVideoAspectRatio() { return aspectRatio; }
public boolean isPaused() {
return !mService.player.getPlayWhenReady();
}
public void showControls(boolean value) {
simpleExoPlayerView.setUseController(value);
}
public void stopVideo() {
if (mBound) {
requireContext().unbindService(mConnection);
mBound = false;
}
}
public void setIsFullscreen(Boolean fullscreen) {
isFullscreen = fullscreen;
TextView fullscreenButton = requireActivity().findViewById(R.id.exo_fullscreen);
if (fullscreen) {
fullscreenButton.setText(R.string.video_compress_icon);
} else {
fullscreenButton.setText(R.string.video_expand_icon);
}
new Iconics.Builder().on(fullscreenButton).build();
}
public Boolean getIsFullscreen() {
return isFullscreen;
}
public void fullScreenToggle() {
if (!isFullscreen) {
setIsFullscreen(true);
requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
} else {
setIsFullscreen(false);
requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
}
/**
* Torrent Playback
*
* @return torrent stream
*/
private TorrentStream setupTorrentStream() {
TorrentOptions torrentOptions = new TorrentOptions.Builder()
.saveLocation(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS))
.removeFilesAfterStop(true)
.build();
TorrentStream torrentStream = TorrentStream.init(torrentOptions);
torrentStream.addListener(new TorrentListener() {
@Override
public void onStreamReady(Torrent torrent) {
String videopath = Uri.fromFile(torrent.getVideoFile()).toString();
Log.d(TAG, "Ready! torrentStream videopath:" + videopath);
mService.setCurrentStreamUrl(videopath, false);
startPlayer();
}
@Override
public void onStreamProgress(Torrent torrent, StreamStatus streamStatus) {
if (streamStatus.bufferProgress <= 100 && progressBar.getProgress() < 100 && progressBar.getProgress() != streamStatus.bufferProgress) {
//Log.d(TAG, "Progress: " + streamStatus.bufferProgress);
progressBar.setProgress(streamStatus.bufferProgress);
}
}
@Override
public void onStreamStopped() {
Log.d(TAG, "Stopped");
}
@Override
public void onStreamPrepared(Torrent torrent) {
Log.d(TAG, "Prepared");
}
@Override
public void onStreamStarted(Torrent torrent) {
Log.d(TAG, "Started");
}
@Override
public void onStreamError(Torrent torrent, Exception e) {
Log.d(TAG, "Error: " + e.getMessage());
}
});
return torrentStream;
}
@Override
public void onVideoEnabled(DecoderCounters counters) {
Log.v(TAG, "onVideoEnabled()...");
}
@Override
public void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs, long initializationDurationMs) {
}
@Override
public void onVideoInputFormatChanged(Format format) {
}
@Override
public void onDroppedFrames(int count, long elapsedMs) {
}
@Override
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
}
@Override
public void onRenderedFirstFrame(Surface surface) {
}
@Override
public void onVideoDisabled(DecoderCounters counters) {
Log.v(TAG, "onVideoDisabled()...");
}
View.OnTouchListener touchListener = new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return mDetector.onTouchEvent(event);
}
};
public String getVideoUuid() {
return mVideoUuid;
}
class MyGestureListener extends GestureDetector.SimpleOnGestureListener {
/*
@Override
public boolean onDown(MotionEvent event) {
Log.d("TAG","onDown: ");
return true;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
Log.i("TAG", "onSingleTapConfirmed: ");
pauseToggle();
return true;
}
@Override
public void onLongPress(MotionEvent e) {
Log.i("TAG", "onLongPress: ");
}
@Override
public boolean onDoubleTap(MotionEvent e) {
Log.i("TAG", "onDoubleTap: ");
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
Log.i("TAG", "onScroll: ");
return true;
}
*/
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public boolean onFling(MotionEvent event1, MotionEvent event2,
float velocityX, float velocityY) {
Log.d(TAG, event1.toString());
Log.d(TAG, event2.toString());
Log.d(TAG, String.valueOf(velocityX));
Log.d(TAG, String.valueOf(velocityY));
//arbitrarily velocity speeds that seem to work to differentiate events.
if (velocityY > 4000) {
Log.d(TAG, "we have a drag down event");
if (canEnterPipMode(getContext())) {
requireActivity().enterPictureInPictureMode();
}
}
if ((velocityX > 2000) && (Math.abs(velocityY) < 2000)) {
Log.d(TAG, "swipe right " + velocityY);
}
if ((velocityX < 2000) && (Math.abs(velocityY) < 2000)) {
Log.d(TAG, "swipe left " + velocityY);
}
return true;
}
}
}

View File

@ -0,0 +1,496 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.fragment
import com.google.android.exoplayer2.video.VideoRendererEventListener
import com.google.android.exoplayer2.ui.PlayerView
import android.content.Intent
import net.schueller.peertube.service.VideoPlayerService
import com.github.se_bastiaan.torrentstream.TorrentStream
import android.widget.LinearLayout
import android.view.GestureDetector
import android.content.ServiceConnection
import android.content.ComponentName
import android.os.IBinder
import net.schueller.peertube.service.VideoPlayerService.LocalBinder
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.AspectRatioListener
import android.view.LayoutInflater
import android.view.ViewGroup
import android.os.Bundle
import net.schueller.peertube.R
import android.app.Activity
import android.app.PictureInPictureParams
import android.content.Context
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
import android.widget.TextView
import android.widget.FrameLayout
import net.schueller.peertube.helper.APIUrlHelper
import net.schueller.peertube.network.GetVideoDataService
import net.schueller.peertube.network.RetrofitInstance
import android.widget.Toast
import net.schueller.peertube.helper.ErrorHelper
import android.content.pm.ActivityInfo
import android.net.Uri
import android.os.Build
import com.github.se_bastiaan.torrentstream.listeners.TorrentListener
import com.github.se_bastiaan.torrentstream.Torrent
import com.github.se_bastiaan.torrentstream.StreamStatus
import com.google.android.exoplayer2.decoder.DecoderCounters
import android.view.View.OnTouchListener
import android.view.MotionEvent
import android.view.GestureDetector.SimpleOnGestureListener
import androidx.annotation.RequiresApi
import android.os.Build.VERSION_CODES
import android.os.Environment
import android.util.Log
import android.view.View
import android.widget.ProgressBar
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.fragment.app.Fragment
import com.github.se_bastiaan.torrentstream.TorrentOptions.Builder
import com.google.android.exoplayer2.Format
import com.google.android.exoplayer2.util.Util
import com.mikepenz.iconics.Iconics
import net.schueller.peertube.R.layout
import net.schueller.peertube.R.string
import net.schueller.peertube.helper.VideoHelper
import net.schueller.peertube.model.Video
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.lang.Exception
import kotlin.math.abs
class VideoPlayerFragment : Fragment(), VideoRendererEventListener {
var videoUuid: String? = null
private set
// private var progressBar: ProgressBar? = null
private var exoPlayer: PlayerView? = null
private var videoPlayerIntent: Intent? = null
private var mBound = false
private var isFullscreen = false
private var mService: VideoPlayerService? = null
private var torrentStream: TorrentStream? = null
// private var torrentStatus: LinearLayout? = null
var videoAspectRatio = 0f
private set
private var mDetector: GestureDetector? = null
private val mConnection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
Log.d(TAG, "onServiceConnected")
val binder = service as LocalBinder
mService = binder.service
// 2. Create the player
exoPlayer!!.player = mService!!.player
mBound = true
loadVideo()
}
override fun onServiceDisconnected(componentName: ComponentName) {
Log.d(TAG, "onServiceDisconnected")
exoPlayer!!.player = null
mBound = false
}
}
private val aspectRatioListener: AspectRatioListener = AspectRatioListener {
targetAspectRatio, _, _ -> videoAspectRatio = targetAspectRatio
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(layout.fragment_video_player, container, false)
}
fun start(videoUuid: String?) {
// start service
val context = context
val activity: Activity? = activity
this.videoUuid = videoUuid
assert(activity != null)
// progressBar = activity?.findViewById(R.id.torrent_progress)
// progressBar?.max = 100
assert(context != null)
exoPlayer = PlayerView(context!!)
exoPlayer = activity?.findViewById(R.id.video_view)
exoPlayer?.controllerShowTimeoutMs = 1000
exoPlayer?.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
mDetector = GestureDetector(context, MyGestureListener())
exoPlayer?.setOnTouchListener(touchListener)
exoPlayer?.setAspectRatioListener(aspectRatioListener)
// torrentStatus = activity?.findViewById(R.id.exo_torrent_status)
// Full screen Icon
val fullscreenText = activity?.findViewById<TextView>(R.id.exo_fullscreen)
val fullscreenButton = activity?.findViewById<FrameLayout>(R.id.exo_fullscreen_button)
fullscreenText?.setText(string.video_expand_icon)
if (fullscreenText != null) {
Iconics.Builder().on(fullscreenText).build()
fullscreenButton?.setOnClickListener {
Log.d(TAG, "Fullscreen")
fullScreenToggle()
}
}
if (!mBound) {
videoPlayerIntent = Intent(context, VideoPlayerService::class.java)
activity?.bindService(videoPlayerIntent, mConnection, Context.BIND_AUTO_CREATE)
}
}
private fun loadVideo() {
val context = context
// get video details from api
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
val service =
RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(context)).create(
GetVideoDataService::class.java
)
val call = service.getVideoData(videoUuid)
call.enqueue(object : Callback<Video?> {
override fun onResponse(call: Call<Video?>, response: Response<Video?>) {
val video = response.body()
mService!!.setCurrentVideo(video)
if (video == null) {
Toast.makeText(
context,
"Unable to retrieve video information, try again later.",
Toast.LENGTH_SHORT
).show()
return
}
playVideo(video)
}
override fun onFailure(call: Call<Video?>, t: Throwable) {
Log.wtf(TAG, t.fillInStackTrace())
ErrorHelper.showToastFromCommunicationError(activity, t)
}
})
}
fun useController(value: Boolean) {
if (mBound) {
exoPlayer!!.useController = value
}
}
private fun playVideo(video: Video) {
val context = context
// video Meta fragment
val videoMetaDataFragment =
(requireActivity().supportFragmentManager.findFragmentById(R.id.video_meta_data_fragment) as VideoMetaDataFragment?)!!
videoMetaDataFragment.updateVideoMeta(video, mService)
val sharedPref = context?.getSharedPreferences(
context.packageName + "_preferences",
Context.MODE_PRIVATE
)
var prefTorrentPlayer = false
var videoQuality = 999999
if (sharedPref != null) {
prefTorrentPlayer = sharedPref.getBoolean(getString(string.pref_torrent_player_key), false)
videoQuality = sharedPref.getInt(getString(string.pref_quality_key), 999999)
}
// if (prefTorrentPlayer) {
// torrentStatus!!.visibility = View.VISIBLE
// val stream = video.files[0].torrentUrl
// Log.v(TAG, "getTorrentUrl : " + video.files[0].torrentUrl)
// torrentStream = setupTorrentStream()
// torrentStream!!.startStream(stream)
// } else {
var urlToPlay: String? = null
var isHLS = false
// try HLS stream first
// get video qualities
// TODO: if auto is set all versions except 0p should be added to a track and have exoplayer auto select optimal bitrate
if (video.streamingPlaylists.size > 0) {
urlToPlay = video.streamingPlaylists[0].playlistUrl
isHLS = true
} else {
if (video.files.size > 0) {
urlToPlay = video.files[0].fileUrl // default, take first found, usually highest res
for (file in video.files) {
// Set quality if it matches
if (file.resolution.id == videoQuality) {
urlToPlay = file.fileUrl
}
}
}
}
if (urlToPlay!!.isNotEmpty()) {
mService!!.setCurrentStreamUrl(urlToPlay, isHLS)
// torrentStatus!!.visibility = View.GONE
startPlayer()
} else {
stopVideo()
Toast.makeText(context, string.api_error, Toast.LENGTH_LONG).show()
}
// }
Log.v(TAG, "end of load Video")
}
private fun startPlayer() {
Util.startForegroundService(requireContext(), videoPlayerIntent!!)
}
fun destroyVideo() {
exoPlayer!!.player = null
if (torrentStream != null) {
torrentStream!!.stopStream()
}
}
fun pauseVideo() {
if (mBound) {
mService!!.player!!.playWhenReady = false
}
}
// fun pauseToggle() {
// if (mBound) {
// mService!!.player!!.playWhenReady = !mService!!.player!!.playWhenReady
// }
// }
fun unPauseVideo() {
if (mBound) {
mService!!.player!!.playWhenReady = true
}
}
val isPaused: Boolean
get() = !mService!!.player!!.playWhenReady
fun showControls(value: Boolean) {
exoPlayer!!.useController = value
}
fun stopVideo() {
if (mBound) {
requireContext().unbindService(mConnection)
mBound = false
}
}
/**
* triggered rotation and button press
*/
fun setIsFullscreen(fullscreen: Boolean) {
isFullscreen = fullscreen
val fullscreenButton = requireActivity().findViewById<TextView>(R.id.exo_fullscreen)
if (fullscreen) {
hideSystemBars()
fullscreenButton.setText(string.video_compress_icon)
} else {
restoreSystemBars()
fullscreenButton.setText(string.video_expand_icon)
}
Iconics.Builder().on(fullscreenButton).build()
}
private fun hideSystemBars()
{
val view = this.view
if (view != null) {
val windowInsetsController =
ViewCompat.getWindowInsetsController(view) ?: return
// Configure the behavior of the hidden system bars
windowInsetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
// Hide both the status bar and the navigation bar
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
}
}
private fun restoreSystemBars()
{
val view = this.view
if (view != null) {
val windowInsetsController =
ViewCompat.getWindowInsetsController(view) ?: return
// Show both the status bar and the navigation bar
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
}
}
fun getIsFullscreen(): Boolean {
return isFullscreen
}
/**
* Triggered by button press
*/
fun fullScreenToggle() {
if (!isFullscreen) {
setIsFullscreen(true)
requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
} else {
setIsFullscreen(false)
// we want to force portrait if fullscreen is switched of as we do not have a min. landscape view
requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
}
//
// /**
// * Torrent Playback
// *
// * @return torrent stream
// */
// private fun setupTorrentStream(): TorrentStream {
// val torrentOptions = Builder()
// .saveLocation(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS))
// .removeFilesAfterStop(true)
// .build()
// val torrentStream = TorrentStream.init(torrentOptions)
// torrentStream.addListener(object : TorrentListener {
// override fun onStreamReady(torrent: Torrent) {
// val videoPath = Uri.fromFile(torrent.videoFile).toString()
// Log.d(TAG, "Ready! torrentStream videoPath:$videoPath")
// mService!!.setCurrentStreamUrl(videoPath, false)
// startPlayer()
// }
//
// override fun onStreamProgress(torrent: Torrent, streamStatus: StreamStatus) {
// if (streamStatus.bufferProgress <= 100 && progressBar!!.progress < 100 && progressBar!!.progress != streamStatus.bufferProgress) {
// //Log.d(TAG, "Progress: " + streamStatus.bufferProgress);
// progressBar!!.progress = streamStatus.bufferProgress
// }
// }
//
// override fun onStreamStopped() {
// Log.d(TAG, "Stopped")
// }
//
// override fun onStreamPrepared(torrent: Torrent) {
// Log.d(TAG, "Prepared")
// }
//
// override fun onStreamStarted(torrent: Torrent) {
// Log.d(TAG, "Started")
// }
//
// override fun onStreamError(torrent: Torrent, e: Exception) {
// Log.d(TAG, "Error: " + e.message)
// }
// })
// return torrentStream
// }
override fun onVideoEnabled(counters: DecoderCounters) {
Log.v(TAG, "onVideoEnabled()...")
}
override fun onVideoDecoderInitialized(
decoderName: String,
initializedTimestampMs: Long,
initializationDurationMs: Long
) {
}
override fun onVideoInputFormatChanged(format: Format) {}
override fun onDroppedFrames(count: Int, elapsedMs: Long) {}
override fun onVideoDisabled(counters: DecoderCounters) {
Log.v(TAG, "onVideoDisabled()...")
}
// touch event on video player
private var touchListener = OnTouchListener { _, event ->
//v.performClick() // causes flicker but should be implemented for accessibility
mDetector!!.onTouchEvent(event)
}
internal inner class MyGestureListener : SimpleOnGestureListener() {
/*
@Override
public boolean onDown(MotionEvent event) {
Log.d("TAG","onDown: ");
return true;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
Log.i("TAG", "onSingleTapConfirmed: ");
pauseToggle();
return true;
}
@Override
public void onLongPress(MotionEvent e) {
Log.i("TAG", "onLongPress: ");
}
@Override
public boolean onDoubleTap(MotionEvent e) {
Log.i("TAG", "onDoubleTap: ");
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
Log.i("TAG", "onScroll: ");
return true;
}
*/
@RequiresApi(api = VERSION_CODES.N)
override fun onFling(
event1: MotionEvent, event2: MotionEvent,
velocityX: Float, velocityY: Float
): Boolean {
Log.d(TAG, event1.toString())
Log.d(TAG, event2.toString())
Log.d(TAG, velocityX.toString())
Log.d(TAG, velocityY.toString())
//arbitrarily velocity speeds that seem to work to differentiate events.
if (velocityY > 4000) {
Log.d(TAG, "we have a drag down event")
if (VideoHelper.canEnterPipMode(context)) {
if (Build.VERSION.SDK_INT >= VERSION_CODES.O) {
val pipParams = PictureInPictureParams.Builder()
requireActivity().enterPictureInPictureMode(pipParams.build())
}
}
}
if (velocityX > 2000 && abs(velocityY) < 2000) {
Log.d(TAG, "swipe right $velocityY")
}
if (velocityX < 2000 && abs(velocityY) < 2000) {
Log.d(TAG, "swipe left $velocityY")
}
return true
}
}
companion object {
private const val TAG = "VideoPlayerFragment"
}
}

View File

@ -1,93 +0,0 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.intents;
import android.Manifest;
import android.app.DownloadManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.webkit.MimeTypeMap;
import android.webkit.URLUtil;
import android.widget.Toast;
import com.github.se_bastiaan.torrentstream.TorrentOptions;
import net.schueller.peertube.R;
import net.schueller.peertube.helper.APIUrlHelper;
import net.schueller.peertube.model.Video;
import androidx.core.app.ActivityCompat;
public class Intents {
private static final String TAG = "Intents";
/**
* https://troll.tv/videos/watch/6edbd9d1-e3c5-4a6c-8491-646e2020469c
*
* @param context context
* @param video video
*/
// TODO, offer which version to download
public static void Share(Context context, Video video) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_SUBJECT, video.getName());
intent.putExtra(Intent.EXTRA_TEXT, APIUrlHelper.getShareUrl(context, video.getUuid()) );
intent.setType("text/plain");
context.startActivity(intent);
}
/**
*
* @param context context
* @param video video
*/
// TODO, offer which version to download
public static void Download(Context context, Video video) {
if (video.getFiles().size() > 0)
{
String url = video.getFiles().get( 0 ).getFileDownloadUrl();
// make sure it is a valid filename
String destFilename = video.getName().replaceAll( "[^a-zA-Z0-9]", "_" ) + "." + MimeTypeMap.getFileExtensionFromUrl( URLUtil.guessFileName( url, null, null ) );
//Toast.makeText(context, destFilename, Toast.LENGTH_LONG).show();
DownloadManager.Request request = new DownloadManager.Request( Uri.parse( url ) );
request.setDescription( video.getDescription() );
request.setTitle( video.getName() );
request.allowScanningByMediaScanner();
request.setNotificationVisibility( DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED );
request.setDestinationInExternalPublicDir( Environment.DIRECTORY_DOWNLOADS, destFilename );
// get download service and enqueue file
DownloadManager manager = (DownloadManager) context.getSystemService( Context.DOWNLOAD_SERVICE );
manager.enqueue( request );
} else {
Toast.makeText( context, R.string.api_error, Toast.LENGTH_LONG ).show();
}
}
}

View File

@ -0,0 +1,146 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.intents
import android.Manifest
import android.app.DownloadManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import net.schueller.peertube.helper.APIUrlHelper
import android.webkit.MimeTypeMap
import android.os.Environment
import android.webkit.URLUtil
import android.widget.Toast
import androidx.core.app.ActivityCompat
import net.schueller.peertube.R
import net.schueller.peertube.model.Video
import android.content.ContextWrapper
import android.app.Activity
object Intents {
private const val TAG = "Intents"
/**
* https://troll.tv/videos/watch/6edbd9d1-e3c5-4a6c-8491-646e2020469c
*
* @param context context
* @param video video
*/
// TODO, offer which version to download
@JvmStatic
fun Share(context: Context, video: Video) {
val intent = Intent()
intent.action = Intent.ACTION_SEND
intent.putExtra(Intent.EXTRA_SUBJECT, video.name)
intent.putExtra(Intent.EXTRA_TEXT, APIUrlHelper.getShareUrl(context, video.uuid))
intent.type = "text/plain"
context.startActivity(intent)
}
/**
* @param context context
* @param video video
*/
// TODO, offer which version to download
fun Download(context: Context, video: Video) {
// deal withe permissions here
// get permission to store file
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
) {
val activity = getActivity(context)
if (activity != null) {
ActivityCompat.requestPermissions(
activity,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
0
)
}
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
) {
startDownload(video, context)
} else {
Toast.makeText(
context,
context.getString(R.string.video_download_permission_error),
Toast.LENGTH_LONG
).show()
}
} else {
startDownload(video, context)
}
}
private fun startDownload(video: Video, context: Context)
{
if (video.files.size > 0) {
val url = video.files[0].fileDownloadUrl
// make sure it is a valid filename
val destFilename = video.name.replace(
"[^a-zA-Z0-9]".toRegex(),
"_"
) + "." + MimeTypeMap.getFileExtensionFromUrl(
URLUtil.guessFileName(url, null, null)
)
//Toast.makeText(context, destFilename, Toast.LENGTH_LONG).show();
val request = DownloadManager.Request(Uri.parse(url))
request.setDescription(video.description)
request.setTitle(video.name)
request.allowScanningByMediaScanner()
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, destFilename)
// get download service and enqueue file
val manager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
manager.enqueue(request)
} else {
Toast.makeText(context, R.string.api_error, Toast.LENGTH_LONG).show()
}
}
private fun getActivity(context: Context?): Activity? {
if (context == null) {
return null
} else if (context is ContextWrapper) {
return if (context is Activity) {
context
} else {
getActivity(context.baseContext)
}
}
return null
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.model
import java.util.*
class Comment(
val id: Int,
val url: String,
val text: String,
val threadId: Int,
val inReplyToCommentId: Int? = null,
val videoId: Int,
val createdAt: Date,
val updatedAt: Date,
val deletedAt: Date? = null,
val isDeleted: Boolean,
val totalRepliesFromVideoAuthor: Int,
val totalReplies: Int,
val account: Account
)

View File

@ -0,0 +1,29 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.model
import com.google.gson.annotations.SerializedName
import net.schueller.peertube.model.ui.OverviewRecycleViewItem
import java.util.ArrayList
class CommentThread(
val total: Int,
val totalNotDeletedComments: Int,
@SerializedName("data")
val comments: ArrayList<Comment>
): OverviewRecycleViewItem()

View File

@ -35,7 +35,7 @@ class Video(
var licence: Licence,
var language: Language,
var nsfw: Boolean,
var description: String,
var description: String? = null,
var local: Boolean,
var live: Boolean,
var duration: Int,
@ -66,7 +66,7 @@ class Video(
companion object {
@JvmStatic
fun getMediaDescription(context: Context?, video: Video): MediaDescriptionCompat {
fun getMediaDescription(video: Video): MediaDescriptionCompat {
// String apiBaseURL = APIUrlHelper.getUrlWithVersion(context);

View File

@ -0,0 +1,7 @@
package net.schueller.peertube.model.ui
import net.schueller.peertube.model.Video
class VideoMetaViewItem: OverviewRecycleViewItem() {
var video: Video? = null
}

View File

@ -16,15 +16,23 @@
*/
package net.schueller.peertube.network;
import com.google.gson.JsonObject;
import net.schueller.peertube.model.Account;
import net.schueller.peertube.model.Channel;
import net.schueller.peertube.model.ChannelList;
import net.schueller.peertube.model.Me;
import net.schueller.peertube.model.VideoList;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.POST;
import retrofit2.http.PUT;
import retrofit2.http.Path;
import retrofit2.http.Query;
@ -52,5 +60,18 @@ public interface GetUserService {
@Path(value = "displayName", encoded = true) String displayName
);
@GET("users/me/subscriptions/exist")
Call<JsonObject> subscriptionsExist(
@Query("uris") String videoChannelNameAndHost
);
@POST("users/me/subscriptions")
Call<ResponseBody> subscribe(
@Body RequestBody params
);
@DELETE("users/me/subscriptions/{videoChannelNameAndHost}")
Call<ResponseBody> unsubscribe(
@Path(value = "videoChannelNameAndHost", encoded = true) String videoChannelNameAndHost
);
}

View File

@ -16,6 +16,7 @@
*/
package net.schueller.peertube.network;
import net.schueller.peertube.model.CommentThread;
import net.schueller.peertube.model.Description;
import net.schueller.peertube.model.Overview;
import net.schueller.peertube.model.Rating;
@ -91,4 +92,12 @@ public interface GetVideoDataService {
@Query("page") int page
);
// https://troll.tv/api/v1/videos/{id}/comment-threads?start=0&count=10&sort=-createdAt
@GET("videos/{id}/comment-threads")
Call<CommentThread> getCommentThreads(
@Path(value = "id", encoded = true) Integer id,
@Query("start") int start,
@Query("count") int count,
@Query("sort") String sort
);
}

View File

@ -1,358 +0,0 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.service;
import static android.media.session.PlaybackState.ACTION_PAUSE;
import static android.media.session.PlaybackState.ACTION_PLAY;
import static com.google.android.exoplayer2.ui.PlayerNotificationManager.ACTION_STOP;
import static net.schueller.peertube.activity.VideoListActivity.EXTRA_VIDEOID;
import static net.schueller.peertube.network.UnsafeOkHttpClient.getUnsafeOkHttpClientBuilder;
import android.app.Notification;
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.graphics.Bitmap;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Binder;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.util.Log;
import android.webkit.URLUtil;
import android.widget.Toast;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator;
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSourceFactory;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.ui.PlayerNotificationManager;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.util.Util;
import net.schueller.peertube.R;
import net.schueller.peertube.activity.VideoPlayActivity;
import net.schueller.peertube.helper.APIUrlHelper;
import net.schueller.peertube.helper.MetaDataHelper;
import net.schueller.peertube.model.Video;
import okhttp3.OkHttpClient;
public class VideoPlayerService extends Service {
private static final String TAG = "VideoPlayerService";
private static final String MEDIA_SESSION_TAG = "peertube_player";
private final IBinder mBinder = new LocalBinder();
private static final String PLAYBACK_CHANNEL_ID = "playback_channel";
private static final Integer PLAYBACK_NOTIFICATION_ID = 1;
public SimpleExoPlayer player;
private Video currentVideo;
private String currentStreamUrl;
private boolean currentStreamUrlIsHLS;
private PlayerNotificationManager playerNotificationManager;
private IntentFilter becomeNoisyIntentFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
private BecomingNoisyReceiver myNoisyAudioStreamReceiver = new BecomingNoisyReceiver();
@Override
public void onCreate() {
Log.v(TAG, "onCreate...");
super.onCreate();
player = new SimpleExoPlayer.Builder(getApplicationContext())
.setTrackSelector(new DefaultTrackSelector(getApplicationContext()))
.build();
// Stop player if audio device changes, e.g. headphones unplugged
player.addListener(new Player.EventListener() {
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
if (playbackState == ACTION_PAUSE) { // this means that pause is available, hence the audio is playing
Log.v(TAG, "ACTION_PLAY: " + playbackState);
registerReceiver(myNoisyAudioStreamReceiver, becomeNoisyIntentFilter);
}
if (playbackState
== ACTION_PLAY) { // this means that play is available, hence the audio is paused or stopped
Log.v(TAG, "ACTION_PAUSE: " + playbackState);
safeUnregisterReceiver();
}
}
});
}
public class LocalBinder extends Binder {
public VideoPlayerService getService() {
// Return this instance of VideoPlayerService so clients can call public methods
return VideoPlayerService.this;
}
}
@Override
public void onDestroy() {
Log.v(TAG, "onDestroy...");
if (playerNotificationManager != null) {
playerNotificationManager.setPlayer(null);
}
//Was seeing an error when exiting the program about not unregistering the receiver.
safeUnregisterReceiver();
if (player != null) {
player.release();
player = null;
}
super.onDestroy();
}
private void safeUnregisterReceiver()
{
try {
unregisterReceiver(myNoisyAudioStreamReceiver);
} catch (Exception e) {
Log.e("VideoPlayerService", "attempted to unregister a nonregistered service");
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Context context = this;
Log.v(TAG, "onStartCommand...");
if (!URLUtil.isValidUrl(currentStreamUrl)) {
Toast.makeText(context, "Invalid URL provided. Unable to play video.", Toast.LENGTH_SHORT).show();
return START_NOT_STICKY;
} else {
playVideo();
return START_STICKY;
}
}
public void setCurrentVideo(Video video) {
Log.v(TAG, "setCurrentVideo...");
currentVideo = video;
}
public void setCurrentStreamUrl(String streamUrl, boolean isHLS) {
Log.v(TAG, "setCurrentStreamUrl..." + streamUrl);
currentStreamUrlIsHLS = isHLS;
currentStreamUrl = streamUrl;
}
//Playback speed control
public void setPlayBackSpeed(float speed) {
Log.v(TAG, "setPlayBackSpeed...");
player.setPlaybackParameters(new PlaybackParameters(speed));
}
/**
* Returns the current playback speed of the player.
*
* @return the current playback speed of the player.
*/
public float getPlayBackSpeed() {
return player.getPlaybackParameters().speed;
}
public void playVideo() {
Context context = this;
// We need a valid URL
Log.v(TAG, "playVideo...");
// Produces DataSource instances through which media data is loaded.
// DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(getApplicationContext(),
// Util.getUserAgent(getApplicationContext(), "PeerTube"), null);
OkHttpClient.Builder okhttpClientBuilder;
if (!APIUrlHelper.useInsecureConnection(this)) {
okhttpClientBuilder = new OkHttpClient.Builder();
} else {
okhttpClientBuilder = getUnsafeOkHttpClientBuilder();
}
// Create a data source factory.
DataSource.Factory dataSourceFactory = new OkHttpDataSourceFactory(okhttpClientBuilder.build(), Util.getUserAgent(getApplicationContext(), "PeerTube"));
// Create a progressive media source pointing to a stream uri.
MediaSource mediaSource;
if (currentStreamUrlIsHLS) {
mediaSource = new HlsMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(Uri.parse(currentStreamUrl)));
} else {
mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(Uri.parse(currentStreamUrl)));
}
// Set the media source to be played.
player.setMediaSource(mediaSource);
// Prepare the player.
player.prepare();
// Auto play
player.setPlayWhenReady(true);
//set playback speed to global default
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
float speed = Float.parseFloat(sharedPref.getString(getString(R.string.pref_video_speed_key), "1.0"));
this.setPlayBackSpeed(speed);
playerNotificationManager = PlayerNotificationManager.createWithNotificationChannel(
context, PLAYBACK_CHANNEL_ID, R.string.playback_channel_name,
PLAYBACK_NOTIFICATION_ID,
new PlayerNotificationManager.MediaDescriptionAdapter() {
@Override
public String getCurrentContentTitle(Player player) {
return currentVideo.getName();
}
@Nullable
@Override
public PendingIntent createCurrentContentIntent(Player player) {
Intent intent = new Intent(context, VideoPlayActivity.class);
intent.putExtra(EXTRA_VIDEOID, currentVideo.getUuid());
return PendingIntent.getActivity(context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
@Override
public String getCurrentContentText(Player player) {
return MetaDataHelper.getMetaString(
currentVideo.getCreatedAt(),
currentVideo.getViews(),
getBaseContext()
);
}
@Nullable
@Override
public Bitmap getCurrentLargeIcon(Player player,
PlayerNotificationManager.BitmapCallback callback) {
return null;
}
}
);
playerNotificationManager.setSmallIcon(R.drawable.ic_logo_bw);
// don't show skip buttons in notification
playerNotificationManager.setUseNavigationActions(false);
playerNotificationManager.setUseStopAction(true);
playerNotificationManager.setNotificationListener(
new PlayerNotificationManager.NotificationListener() {
@Override
public void onNotificationStarted(int notificationId, Notification notification) {
startForeground(notificationId, notification);
}
@Override
public void onNotificationCancelled(int notificationId) {
Log.v(TAG, "onNotificationCancelled...");
stopForeground(true);
Intent killFloat = new Intent(ACTION_STOP);
sendBroadcast(killFloat);
/*
Intent killFloat = new Intent(BROADCAST_ACTION);
Intent killFloatingWindow = new Intent(getApplicationContext(),VideoPlayActivity.class);
killFloatingWindow.putExtra("killFloat",true);
startActivity(killFloatingWindow);
// TODO: only kill the notification if we no longer have a bound activity
stopForeground(true);
*/
}
}
);
playerNotificationManager.setPlayer(player);
// external Media control, Android Wear / Google Home etc.
MediaSessionCompat mediaSession = new MediaSessionCompat(context, MEDIA_SESSION_TAG);
mediaSession.setActive(true);
playerNotificationManager.setMediaSessionToken(mediaSession.getSessionToken());
MediaSessionConnector mediaSessionConnector = new MediaSessionConnector(mediaSession);
mediaSessionConnector.setQueueNavigator(new TimelineQueueNavigator(mediaSession) {
@Override
public MediaDescriptionCompat getMediaDescription(Player player, int windowIndex) {
return Video.getMediaDescription(context, currentVideo);
}
});
mediaSessionConnector.setPlayer(player);
// Audio Focus
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.CONTENT_TYPE_MOVIE)
.build();
player.setAudioAttributes(audioAttributes, true);
}
// pause playback on audio output change
private class BecomingNoisyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
player.setPlayWhenReady(false);
}
}
}
}

View File

@ -0,0 +1,312 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.service
import android.app.Notification
import net.schueller.peertube.helper.MetaDataHelper.getMetaString
import net.schueller.peertube.model.Video.Companion.getMediaDescription
import android.os.IBinder
import com.google.android.exoplayer2.ui.PlayerNotificationManager
import android.content.IntentFilter
import android.media.AudioManager
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import android.media.session.PlaybackState
import android.content.Intent
import android.widget.Toast
import net.schueller.peertube.helper.APIUrlHelper
import net.schueller.peertube.network.UnsafeOkHttpClient
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.hls.HlsMediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.ui.PlayerNotificationManager.MediaDescriptionAdapter
import android.app.PendingIntent
import android.app.Service
import net.schueller.peertube.activity.VideoPlayActivity
import net.schueller.peertube.activity.VideoListActivity
import android.graphics.Bitmap
import android.support.v4.media.session.MediaSessionCompat
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
import android.support.v4.media.MediaDescriptionCompat
import android.content.BroadcastReceiver
import android.content.Context
import android.net.Uri
import android.os.Binder
import android.util.Log
import android.webkit.URLUtil
import androidx.core.app.NotificationCompat
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource
import com.google.android.exoplayer2.ui.PlayerNotificationManager.NotificationListener
import net.schueller.peertube.R.drawable
import net.schueller.peertube.R.string
import net.schueller.peertube.model.Video
import java.lang.Exception
class VideoPlayerService : Service() {
private val mBinder: IBinder = LocalBinder()
@JvmField
var player: ExoPlayer? = null
private var currentVideo: Video? = null
private var currentStreamUrl: String? = null
private var currentStreamUrlIsHLS = false
private var playerNotificationManager: PlayerNotificationManager? = null
private val becomeNoisyIntentFilter = IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
private val myNoisyAudioStreamReceiver = BecomingNoisyReceiver()
override fun onCreate() {
Log.v(TAG, "onCreate...")
super.onCreate()
player = ExoPlayer.Builder(applicationContext)
.setTrackSelector(DefaultTrackSelector(applicationContext))
.build()
// Stop player if audio device changes, e.g. headphones unplugged
player!!.addListener(object : Player.Listener {
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
if (playbackState.toLong() == PlaybackState.ACTION_PAUSE) { // this means that pause is available, hence the audio is playing
Log.v(TAG, "ACTION_PLAY: $playbackState")
registerReceiver(myNoisyAudioStreamReceiver, becomeNoisyIntentFilter)
}
if (playbackState
.toLong() == PlaybackState.ACTION_PLAY
) { // this means that play is available, hence the audio is paused or stopped
Log.v(TAG, "ACTION_PAUSE: $playbackState")
safeUnregisterReceiver()
}
}
})
}
inner class LocalBinder : Binder() {
// Return this instance of VideoPlayerService so clients can call public methods
val service: VideoPlayerService
get() =// Return this instance of VideoPlayerService so clients can call public methods
this@VideoPlayerService
}
override fun onDestroy() {
Log.v(TAG, "onDestroy...")
if (playerNotificationManager != null) {
playerNotificationManager!!.setPlayer(null)
}
//Was seeing an error when exiting the program about not unregistering the receiver.
safeUnregisterReceiver()
if (player != null) {
player!!.release()
player = null
}
super.onDestroy()
}
private fun safeUnregisterReceiver() {
try {
unregisterReceiver(myNoisyAudioStreamReceiver)
} catch (e: Exception) {
Log.e("VideoPlayerService", "attempted to unregister a non-registered service")
}
}
override fun onBind(intent: Intent): IBinder {
return mBinder
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val context: Context = this
Log.v(TAG, "onStartCommand...")
return if (!URLUtil.isValidUrl(currentStreamUrl)) {
Toast.makeText(context, "Invalid URL provided. Unable to play video.", Toast.LENGTH_SHORT).show()
START_NOT_STICKY
} else {
playVideo()
START_STICKY
}
}
fun setCurrentVideo(video: Video?) {
Log.v(TAG, "setCurrentVideo...")
currentVideo = video
}
fun setCurrentStreamUrl(streamUrl: String, isHLS: Boolean) {
Log.v(TAG, "setCurrentStreamUrl...$streamUrl")
currentStreamUrlIsHLS = isHLS
currentStreamUrl = streamUrl
}
/**
* Returns the current playback speed of the player.
*
* @return the current playback speed of the player.
*///Playback speed control
var playBackSpeed: Float
get() = player!!.playbackParameters.speed
set(speed) {
Log.v(TAG, "setPlayBackSpeed...")
player!!.playbackParameters = PlaybackParameters(speed)
}
private fun playVideo() {
val context: Context = this
// We need a valid URL
Log.v(TAG, "playVideo...")
// Produces DataSource instances through which media data is loaded.
val okhttpClientBuilder: okhttp3.OkHttpClient.Builder = if (!APIUrlHelper.useInsecureConnection(this)) {
okhttp3.OkHttpClient.Builder()
} else {
UnsafeOkHttpClient.getUnsafeOkHttpClientBuilder()
}
// Create a data source factory.
val dataSourceFactory: OkHttpDataSource.Factory = OkHttpDataSource.Factory(
okhttpClientBuilder.build()
)
// Create a progressive media source pointing to a stream uri.
val mediaSource: MediaSource = if (currentStreamUrlIsHLS) {
HlsMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(Uri.parse(currentStreamUrl)))
} else {
ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(Uri.parse(currentStreamUrl)))
}
// Set the media source to be played.
player!!.setMediaSource(mediaSource)
// Prepare the player.
player!!.prepare()
// Auto play
player!!.playWhenReady = true
//set playback speed to global default
val sharedPref = getSharedPreferences(
packageName + "_preferences",
Context.MODE_PRIVATE
)
val speed = sharedPref.getString(getString(string.pref_video_speed_key), "1.0")!!.toFloat()
playBackSpeed = speed
playerNotificationManager = PlayerNotificationManager.Builder(
this,
PLAYBACK_NOTIFICATION_ID,
PLAYBACK_CHANNEL_ID,
).setMediaDescriptionAdapter(
object : MediaDescriptionAdapter {
override fun getCurrentContentTitle(player: Player): CharSequence {
return currentVideo!!.name
}
override fun createCurrentContentIntent(player: Player): PendingIntent? {
val intent = Intent(context, VideoPlayActivity::class.java)
intent.putExtra(VideoListActivity.EXTRA_VIDEOID, currentVideo!!.uuid)
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
override fun getCurrentContentText(player: Player): CharSequence {
return getMetaString(
currentVideo!!.createdAt,
currentVideo!!.views,
baseContext
)
}
override fun getCurrentSubText(player: Player): CharSequence { return ""}
override fun getCurrentLargeIcon(
player: Player,
callback: PlayerNotificationManager.BitmapCallback
): Bitmap? {
return null
}
}
).setNotificationListener(
object : NotificationListener {
override fun onNotificationPosted(notificationId: Int, notification: Notification, ongoing: Boolean) {
super.onNotificationPosted(notificationId, notification, ongoing)
if (ongoing) // allow notification to be dismissed if player is stopped
startForeground(notificationId, notification)
else
stopForeground(false)
}
override fun onNotificationCancelled(notificationId: Int, dismissedByUser: Boolean) {
super.onNotificationCancelled(notificationId, dismissedByUser)
stopSelf()
stopForeground(true)
}
}
).setChannelNameResourceId(string.playback_channel_name)
.setChannelDescriptionResourceId(string.playback_channel_description)
.build()
playerNotificationManager!!.setPriority(NotificationCompat.PRIORITY_DEFAULT)
playerNotificationManager!!.setSmallIcon(drawable.ic_logo_bw)
// don't show skip buttons in notification
playerNotificationManager!!.setUseNextAction(false)
playerNotificationManager!!.setUsePreviousAction(false)
playerNotificationManager!!.setPlayer(player)
// external Media control, Android Wear / Google Home etc.
val mediaSession = MediaSessionCompat(context, MEDIA_SESSION_TAG)
mediaSession.isActive = true
playerNotificationManager!!.setMediaSessionToken(mediaSession.sessionToken)
val mediaSessionConnector = MediaSessionConnector(mediaSession)
mediaSessionConnector.setQueueNavigator(object : TimelineQueueNavigator(mediaSession) {
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
return getMediaDescription(currentVideo!!)
}
})
mediaSessionConnector.setPlayer(player)
// Audio Focus
val audioAttributes = AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.CONTENT_TYPE_MOVIE)
.build()
player!!.setAudioAttributes(audioAttributes, true)
}
// pause playback on audio output change
private inner class BecomingNoisyReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (AudioManager.ACTION_AUDIO_BECOMING_NOISY == intent.action) {
player!!.playWhenReady = false
}
}
}
companion object {
private const val TAG = "VideoPlayerService"
private const val MEDIA_SESSION_TAG = "peertube_player"
private const val PLAYBACK_CHANNEL_ID = "playback_channel"
private const val PLAYBACK_NOTIFICATION_ID = 1
}
}

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M6,9l6,6l6,-6"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M18,6L6,18"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M6,6L18,18"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,27 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M21,15v4a2,2 0,0 1,-2 2H5a2,2 0,0 1,-2 -2v-4"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M7,10l5,5l5,-5"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M12,15L12,3"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,11 @@
<vector android:height="52dp" android:viewportHeight="24"
android:viewportWidth="24" android:width="52dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000"
android:pathData="M13,19l9,-7l-9,-7l0,14z"
android:strokeColor="#ffffff" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
<path android:fillColor="#00000000"
android:pathData="M2,19l9,-7l-9,-7l0,14z"
android:strokeColor="#ffffff" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
</vector>

View File

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4,15s1,-1 4,-1 5,2 8,2 4,-1 4,-1V3s-1,1 -4,1 -5,-2 -8,-2 -4,1 -4,1z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M4,22L4,15"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector android:height="52dp" android:viewportHeight="24"
android:viewportWidth="24" android:width="52dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000" android:pathData="M6,4h4v16h-4z"
android:strokeColor="#ffffff" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
<path android:fillColor="#00000000"
android:pathData="M14,4h4v16h-4z"
android:strokeColor="#ffffff" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
</vector>

View File

@ -0,0 +1,7 @@
<vector android:height="52dp" android:viewportHeight="24"
android:viewportWidth="24" android:width="52dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000"
android:pathData="M5,3l14,9l-14,9l0,-18z"
android:strokeColor="#ffffff" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:viewportHeight="426.7"
android:viewportWidth="426.7" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#ffffff" android:pathData="M0,64h256v42.7H0zM0,149.3h256V192H0zM0,234.7h170.7v42.7H0z"/>
<path android:fillColor="#ffffff" android:pathData="M341.3,234.7v-85.4h-42.6v85.4h-85.4v42.6h85.4v85.4h42.6v-85.4h85.4v-42.6z"/>
</vector>

View File

@ -0,0 +1,11 @@
<vector android:height="52dp" android:viewportHeight="24"
android:viewportWidth="24" android:width="52dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000"
android:pathData="M11,19l-9,-7l9,-7l0,14z"
android:strokeColor="#ffffff" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
<path android:fillColor="#00000000"
android:pathData="M22,19l-9,-7l9,-7l0,14z"
android:strokeColor="#ffffff" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
</vector>

View File

@ -0,0 +1,41 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M18,5m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M6,12m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M18,19m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M8.59,13.51L15.42,17.49"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M15.41,6.51L8.59,10.49"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,12m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M4.93,4.93L19.07,19.07"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M10,15v4a3,3 0,0 0,3 3l4,-9L17,2L5.72,2a2,2 0,0 0,-2 1.7l-1.38,9a2,2 0,0 0,2 2.3zM17,2h2.67A2.31,2.31 0,0 1,22 4v7a2.31,2.31 0,0 1,-2.33 2L17,13"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M10,15v4a3,3 0,0 0,3 3l4,-9L17,2L5.72,2a2,2 0,0 0,-2 1.7l-1.38,9a2,2 0,0 0,2 2.3zM17,2h2.67A2.31,2.31 0,0 1,22 4v7a2.31,2.31 0,0 1,-2.33 2L17,13"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#ffffff"
android:strokeColor="#555555"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M14,9V5a3,3 0,0 0,-3 -3l-4,9v11h11.28a2,2 0,0 0,2 -1.7l1.38,-9a2,2 0,0 0,-2 -2.3zM7,22H4a2,2 0,0 1,-2 -2v-7a2,2 0,0 1,2 -2h3"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M14,9V5a3,3 0,0 0,-3 -3l-4,9v11h11.28a2,2 0,0 0,2 -1.7l1.38,-9a2,2 0,0 0,-2 -2.3zM7,22H4a2,2 0,0 1,-2 -2v-7a2,2 0,0 1,2 -2h3"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#ffffff"
android:strokeColor="#555555"
android:strokeLineCap="round"/>
</vector>

View File

@ -6,36 +6,31 @@
android:keepScreenOn="true"
tools:context="net.schueller.peertube.activity.VideoPlayActivity">
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment android:name="net.schueller.peertube.fragment.VideoPlayerFragment"
<fragment
android:id="@+id/video_player_fragment"
android:name="net.schueller.peertube.fragment.VideoPlayerFragment"
android:layout_width="match_parent"
android:layout_height="250dp" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/video_player_fragment"
android:layout_marginTop="250dp"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/video_player_fragment"
android:orientation="vertical">
<ScrollView
android:id="@+id/login_form"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment android:name="net.schueller.peertube.fragment.VideoMetaDataFragment"
android:id="@+id/video_meta_data_fragment"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</ScrollView>
</RelativeLayout>
<fragment
android:id="@+id/video_meta_data_fragment"
android:name="net.schueller.peertube.fragment.VideoMetaDataFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</RelativeLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,225 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:layout_width="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="?android:colorBackground"
android:clickable="true"
android:focusable="true"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingTop="12dp"
android:paddingEnd="12dp"
android:paddingBottom="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:gravity="start"
android:text="@string/video_meta_title_description"
android:textSize="24sp" />
<ImageButton
android:id="@+id/video_description_close_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:width="24dp"
android:height="24dp"
android:background="@android:color/transparent"
android:gravity="end"
android:src="@drawable/ic_close"
app:tint="?attr/colorPrimary" />
</RelativeLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingTop="12dp"
android:paddingEnd="12dp">
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_marginStart="18dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:autoLink="web"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Body1" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/description"
android:layout_marginStart="18dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="bottom"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_privacy"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<Space
android:layout_width="12dp"
android:layout_height="1dp" />
<TextView
android:id="@+id/video_privacy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|start"
android:lines="2"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
tools:text="@tools:sample/lorem/random" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="bottom"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_category"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<Space
android:layout_width="12dp"
android:layout_height="1dp" />
<TextView
android:id="@+id/video_category"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|start"
android:lines="2"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
tools:text="@tools:sample/lorem/random" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="bottom"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_license"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<Space
android:layout_width="12dp"
android:layout_height="1dp" />
<TextView
android:id="@+id/video_license"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|start"
android:lines="2"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
tools:text="@tools:sample/lorem" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="bottom"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_language"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<Space
android:layout_width="12dp"
android:layout_height="1dp" />
<TextView
android:id="@+id/video_language"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|start"
android:lines="2"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
tools:text="@tools:sample/lorem/random" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="bottom"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_tags"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<Space
android:layout_width="12dp"
android:layout_height="1dp" />
<TextView
android:id="@+id/video_tags"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|start"
android:lines="2"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
tools:text="@tools:sample/lorem/random" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</ScrollView>
</LinearLayout>

View File

@ -1,364 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="6dp">
android:id="@+id/videoMetaFragment"
android:layout_height="wrap_content">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/avatar"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_alignParentStart="true"
android:layout_marginTop="0dp"
android:contentDescription="@string/video_row_account_avatar"
android:paddingStart="12dp"
android:paddingTop="12dp"
android:paddingEnd="12dp" />
<TextView
android:id="@+id/sl_row_name"
<!-- Related Videos -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/relatedVideosView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginStart="6dp"
android:layout_marginTop="0dp"
android:layout_marginEnd="24dp"
android:layout_toEndOf="@+id/avatar"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title" />
android:layout_height="match_parent">
<TextView
android:id="@+id/videoMeta"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/sl_row_name"
android:layout_alignParentEnd="true"
android:layout_marginStart="6dp"
android:layout_marginTop="0dp"
android:layout_marginEnd="12dp"
android:layout_toEndOf="@+id/avatar"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />
<TextView
android:id="@+id/videoOwner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/videoMeta"
android:layout_marginStart="6dp"
android:layout_marginTop="0dp"
android:layout_marginEnd="0dp"
android:layout_toEndOf="@id/avatar"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />
<TextView
android:id="@+id/moreButton"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_marginStart="-16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="0dp"
android:layout_toEndOf="@+id/sl_row_name"
android:background="@null"
android:contentDescription="@string/descr_overflow_button"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />
<LinearLayout
android:id="@+id/video_actions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/videoOwner"
android:layout_marginStart="18dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="65dp"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/video_thumbs_up"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="?android:selectableItemBackground"
android:gravity="center" />
<TextView
android:id="@+id/video_thumbs_up_total"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="12sp" />
</LinearLayout>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="65dp"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/video_thumbs_down"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="?android:selectableItemBackground"
android:gravity="center" />
<TextView
android:id="@+id/video_thumbs_down_total"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="12sp" />
</LinearLayout>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="65dp"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/video_share"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="?android:selectableItemBackground"
android:gravity="center" />
<TextView
android:id="@+id/video_share_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_share"
android:textSize="12sp" />
</LinearLayout>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="65dp"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/video_download"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="?android:selectableItemBackground"
android:gravity="center" />
<TextView
android:id="@+id/video_download_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_download"
android:textSize="12sp" />
</LinearLayout>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
</LinearLayout>
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/video_actions"
android:layout_alignParentStart="true"
android:layout_marginStart="18dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:autoLink="web"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Body1" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/description"
android:layout_marginStart="18dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="bottom"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_privacy"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<Space
android:layout_width="12dp"
android:layout_height="1dp" />
<TextView
android:id="@+id/video_privacy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|start"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
tools:text="@tools:sample/lorem/random"
android:lines="2"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="bottom"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_category"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<Space
android:layout_width="12dp"
android:layout_height="1dp" />
<TextView
android:id="@+id/video_category"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|start"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
tools:text="@tools:sample/lorem/random"
android:lines="2"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="bottom"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_license"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<Space
android:layout_width="12dp"
android:layout_height="1dp" />
<TextView
android:id="@+id/video_license"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|start"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
tools:text="@tools:sample/lorem"
android:lines="2"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="bottom"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_language"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<Space
android:layout_width="12dp"
android:layout_height="1dp" />
<TextView
android:id="@+id/video_language"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|start"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
tools:text="@tools:sample/lorem/random"
android:lines="2"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="bottom"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_tags"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<Space
android:layout_width="12dp"
android:layout_height="1dp" />
<TextView
android:id="@+id/video_tags"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|start"
tools:text="@tools:sample/lorem/random"
android:lines="2"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />
</LinearLayout>
</LinearLayout>
</androidx.recyclerview.widget.RecyclerView>
</RelativeLayout>

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="6dp"
android:padding="6dp"
android:id="@+id/video_title_block"
>
<RelativeLayout
android:id="@+id/video_comments_title_wrapper"
android:layout_marginStart="6dp"
android:layout_marginEnd="6dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/video_comments_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerInParent="true"
android:layout_centerVertical="true"
android:layout_marginStart="0dp"
android:layout_marginTop="6dp"
android:text="@string/video_comments_title"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<TextView
android:id="@+id/video_comments_total_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginStart="6dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="24dp"
android:layout_toEndOf="@+id/video_comments_title"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<ImageButton
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="@android:color/transparent"
android:clickable="false"
android:contentDescription="@string/video_meta_show_description"
android:src="@drawable/ic_chevron_down"
app:tint="?attr/colorPrimary" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/video_highlighted_comment_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/video_comments_title_wrapper">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/video_highlighted_avatar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/video_highlighted_comment"
android:layout_toEndOf="@+id/video_highlighted_avatar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true"
android:layout_marginStart="6dp"
android:textSize="12sp"
android:layout_marginTop="0dp"
android:layout_marginEnd="6dp"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />
</RelativeLayout>
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,402 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<!-- Video Title Block -->
<RelativeLayout
android:id="@+id/video_title_block"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="6dp"
android:paddingTop="6dp">
<RelativeLayout
android:id="@+id/video_open_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginEnd="6dp">
<TextView
android:id="@+id/video_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_marginStart="0dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="24dp"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title" />
<ImageButton
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="@android:color/transparent"
android:clickable="false"
android:contentDescription="@string/video_meta_show_description"
android:src="@drawable/ic_chevron_down"
app:tint="?attr/colorPrimary" />
</RelativeLayout>
<TextView
android:id="@+id/videoMeta"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/video_open_description"
android:layout_alignParentEnd="true"
android:layout_marginStart="6dp"
android:layout_marginTop="0dp"
android:layout_marginEnd="6dp"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />
</RelativeLayout>
<!-- video actions -->
<HorizontalScrollView
android:id="@+id/video_actions_block"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/video_title_block"
android:paddingBottom="6dp"
android:scrollbars="none">
<LinearLayout
android:id="@+id/video_actions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:layout_marginEnd="18dp"
android:paddingEnd="18dp"
android:paddingStart="18dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="70dp"
android:id="@+id/video_thumbs_up_wrapper"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageButton
android:id="@+id/video_thumbs_up"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_gravity="center"
android:background="?android:selectableItemBackground"
android:gravity="center"
android:clickable="false"
android:src="@drawable/ic_thumbs_up"
app:tint="?attr/colorPrimary" />
<TextView
android:id="@+id/video_thumbs_up_total"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Button"
android:textSize="12sp" />
</LinearLayout>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="70dp"
android:id="@+id/video_thumbs_down_wrapper"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageButton
android:id="@+id/video_thumbs_down"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_gravity="center"
android:background="?android:selectableItemBackground"
android:gravity="center"
android:clickable="false"
android:src="@drawable/ic_thumbs_down"
app:tint="?attr/colorPrimary" />
<TextView
android:id="@+id/video_thumbs_down_total"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Button"
android:textSize="12sp" />
</LinearLayout>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="70dp"
android:id="@+id/video_share_wrapper"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageButton
android:id="@+id/video_share"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_gravity="center"
android:background="?android:selectableItemBackground"
android:gravity="center"
android:clickable="false"
android:src="@drawable/ic_share_2"
app:tint="?attr/colorPrimary" />
<TextView
android:id="@+id/video_share_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_share"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Button"
android:textSize="12sp" />
</LinearLayout>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="70dp"
android:id="@+id/video_download_wrapper"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageButton
android:id="@+id/video_download"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_gravity="center"
android:background="?android:selectableItemBackground"
android:gravity="center"
android:clickable="false"
android:src="@drawable/ic_download"
app:tint="?attr/colorPrimary" />
<TextView
android:id="@+id/video_download_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_download"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Button"
android:textSize="12sp" />
</LinearLayout>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="70dp"
android:id="@+id/video_add_to_playlist_wrapper"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageButton
android:id="@+id/video_add_to_playlist"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_gravity="center"
android:background="?android:selectableItemBackground"
android:gravity="center"
android:clickable="false"
android:src="@drawable/ic_playlist_add"
app:tint="?attr/colorPrimary" />
<TextView
android:id="@+id/video_add_to_playlist_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_add_to_playlist"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Button"
android:textSize="12sp" />
</LinearLayout>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="70dp"
android:id="@+id/video_block_wrapper"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageButton
android:id="@+id/video_block"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_gravity="center"
android:background="?android:selectableItemBackground"
android:gravity="center"
android:clickable="false"
android:src="@drawable/ic_slash"
app:tint="?attr/colorPrimary" />
<TextView
android:id="@+id/vvideo_block_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_block"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Button"
android:textSize="12sp" />
</LinearLayout>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="70dp"
android:id="@+id/video_flag_wrapper"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageButton
android:id="@+id/video_flag"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_gravity="center"
android:background="?android:selectableItemBackground"
android:gravity="center"
android:clickable="false"
android:src="@drawable/ic_flag"
app:tint="?attr/colorPrimary" />
<TextView
android:id="@+id/vvideo_flag_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_flag"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Button"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
</HorizontalScrollView>
<TextView
android:layout_below="@+id/video_actions_block"
android:id="@+id/video_action_block_line"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:background="?android:colorEdgeEffect"
android:height="1dp"
android:gravity="center_horizontal"/>
<RelativeLayout
android:id="@+id/video_account_block"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/video_action_block_line"
android:paddingStart="12dp"
android:paddingTop="6dp"
android:paddingEnd="6dp">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/avatar"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:contentDescription="@string/video_row_account_avatar"
android:paddingStart="12dp"
android:paddingTop="12dp"
android:paddingEnd="12dp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="12dp"
android:layout_toEndOf="@+id/avatar"
android:orientation="vertical">
<TextView
android:id="@+id/videoOwner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<TextView
android:id="@+id/videoOwnerSubscribers"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />
</LinearLayout>
<TextView
android:id="@+id/videoOwnerSubscribeButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginTop="0dp"
android:layout_marginEnd="0dp"
android:gravity="end"
android:text=""
android:textAppearance="@style/Base.TextAppearance.AppCompat.Button" />
<!-- <TextView-->
<!-- android:id="@+id/moreButton"-->
<!-- android:layout_width="45dp"-->
<!-- android:layout_height="45dp"-->
<!-- android:layout_marginStart="-16dp"-->
<!-- android:layout_marginTop="16dp"-->
<!-- android:layout_marginEnd="0dp"-->
<!-- android:layout_toEndOf="@+id/sl_row_name"-->
<!-- android:background="@null"-->
<!-- android:contentDescription="@string/descr_overflow_button"-->
<!-- android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />-->
</RelativeLayout>
<TextView
android:layout_below="@+id/video_account_block"
android:id="@+id/video_account_block_line"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:background="?android:colorEdgeEffect"
android:height="1dp"
android:gravity="center_horizontal"/>
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -8,7 +8,7 @@
android:background="#CC000000"
android:layoutDirection="ltr"
android:orientation="vertical"
tools:targetApi="28">
tools:targetApi="32">
<FrameLayout
android:id="@+id/exo_more_button"
@ -18,14 +18,14 @@
<TextView
android:id="@+id/exo_more"
android:layout_width="18dp"
android:layout_width="24dp"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:paddingTop="12dp"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:textColor="#FFBEBEBE"
android:textSize="12sp" />
android:textSize="18sp" />
</FrameLayout>
@ -43,27 +43,63 @@
android:gravity="center"
android:orientation="horizontal"
android:paddingTop="8dp">
<Space
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<ImageButton
android:id="@id/exo_rew"
style="@style/ExoMediaButton.Rewind" />
android:layout_width="72sp"
android:layout_height="52sp"
android:layout_gravity="start"
android:background="@android:color/transparent"
android:contentDescription="@string/exo_controls_rewind_description"
android:scaleType="center"
android:src="@drawable/ic_rewind" />
<Space
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<ImageButton
android:id="@id/exo_repeat_toggle"
style="@style/ExoMediaButton" />
<ImageButton
android:id="@id/exo_play"
style="@style/ExoMediaButton.Play" />
android:contentDescription="@string/exo_controls_play_description"
android:src="@drawable/ic_play"
android:layout_height="52sp"
android:layout_width="72sp"
android:scaleType="center"
android:background="@android:color/transparent"
/>
<ImageButton
android:id="@id/exo_pause"
style="@style/ExoMediaButton.Pause" />
android:contentDescription="@string/exo_controls_pause_description"
android:src="@drawable/ic_pause"
android:layout_height="52sp"
android:layout_width="72sp"
android:scaleType="center"
android:background="@android:color/transparent"
/>
<Space
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<ImageButton
android:id="@id/exo_ffwd"
style="@style/ExoMediaButton.FastForward" />
android:layout_width="72sp"
android:layout_height="52sp"
android:layout_gravity="end"
android:background="@android:color/transparent"
android:contentDescription="@string/exo_controls_fastforward_description"
android:scaleType="center"
android:src="@drawable/ic_fast_forward" />
<Space
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
@ -86,9 +122,29 @@
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:layout_gravity="center"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:textColor="#FFBEBEBE"
android:paddingStart="12dp"
android:paddingEnd="2dp"
android:textColor="#FFFFFF"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="#BABABA"
android:textSize="14sp"
android:includeFontPadding="false"
android:text="@string/player_time_seperator" />
<TextView
android:id="@id/exo_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:includeFontPadding="false"
android:paddingStart="2dp"
android:paddingEnd="6dp"
android:textColor="#BABABA"
android:textSize="14sp" />
<View
@ -96,18 +152,6 @@
android:layout_height="0dp"
android:layout_weight="1" />
<TextView
android:id="@id/exo_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:layout_gravity="center"
android:paddingLeft="6dp"
android:paddingRight="6dp"
android:textColor="#FFBEBEBE"
android:textSize="14sp" />
<FrameLayout
android:id="@+id/exo_fullscreen_button"
@ -117,13 +161,13 @@
<TextView
android:id="@+id/exo_fullscreen"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:textColor="#FFBEBEBE"
android:textSize="12sp" />
android:textSize="18sp" />
</FrameLayout>
@ -146,20 +190,20 @@
</LinearLayout>
<LinearLayout
android:visibility="gone"
android:id="@+id/exo_torrent_status"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- <LinearLayout-->
<!-- android:visibility="gone"-->
<!-- android:id="@+id/exo_torrent_status"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content">-->
<ProgressBar
android:id="@+id/torrent_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="false"
android:max="100" />
<!-- <ProgressBar-->
<!-- android:id="@+id/torrent_progress"-->
<!-- style="?android:attr/progressBarStyleHorizontal"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:indeterminate="false"-->
<!-- android:max="100" />-->
</LinearLayout>
<!-- </LinearLayout>-->
</LinearLayout>

View File

@ -64,6 +64,7 @@
<string name="title_activity_video_play" translatable="false">VideoPlayActivity</string>
<string name="playback_channel_name" translatable="false">PeerTube</string>
<string name="playback_channel_description" translatable="false">playback_channel</string>
<string name="peertube_instance_search_default_description" translatable="false">PeerTube, a federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.</string>

View File

@ -366,4 +366,14 @@
<string name="video_list_live_marker">LIVE</string>
<string name="video_get_full_description_failed">Getting full video description failed</string>
<string name="video_description_read_more">Read More</string>
<string name="player_time_seperator">/</string>
<string name="video_meta_show_description">Show Description</string>
<string name="video_meta_title_description">Description</string>
<string name="video_add_to_playlist">Save</string>
<string name="video_block">Block</string>
<string name="video_flag">Flag</string>
<string name="video_feature_not_yet_implemented">This feature has not yet been implemented. Coming soon!</string>
<string name="subscribe">Subscribe</string>
<string name="unsubscribe">Unsubscribe</string>
<string name="video_comments_title">Comments</string>
</resources>

View File

@ -76,12 +76,12 @@
app:title="@string/pref_background_behavior"
app:iconSpaceReserved="false"/>
<SwitchPreference
app:defaultValue="false"
app:key="@string/pref_torrent_player_key"
app:summary="@string/pref_description_torrent_player"
app:title="@string/pref_title_torrent_player"
app:iconSpaceReserved="false"/>
<!-- <SwitchPreference-->
<!-- app:defaultValue="false"-->
<!-- app:key="@string/pref_torrent_player_key"-->
<!-- app:summary="@string/pref_description_torrent_player"-->
<!-- app:title="@string/pref_title_torrent_player"-->
<!-- app:iconSpaceReserved="false"/>-->
</PreferenceCategory>

View File

@ -2,11 +2,11 @@
buildscript {
ext.kotlin_version = '1.5.31'
ext.kotlin_version = '1.6.10'
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.4'
@ -21,7 +21,6 @@ buildscript {
allprojects {
repositories {
google()
jcenter()
mavenCentral()
maven {
url 'https://oss.sonatype.org/content/repositories/snapshots'