Background media controls #242
10 changed files with 183 additions and 6 deletions
|
@ -228,7 +228,7 @@ class AppWithNavigationState extends React.Component {
|
|||
}
|
||||
|
||||
_handleAppStateChange = (nextAppState) => {
|
||||
const { dispatch } = this.props;
|
||||
const { backgroundPlayEnabled, dispatch } = this.props;
|
||||
// Check if the app was suspended
|
||||
if (AppState.currentState && AppState.currentState.match(/inactive|background/)) {
|
||||
AsyncStorage.getItem('firstLaunchTime').then(start => {
|
||||
|
@ -237,12 +237,21 @@ class AppWithNavigationState extends React.Component {
|
|||
// If so, this needs to be included as a property when tracking
|
||||
AsyncStorage.setItem('firstLaunchSuspended', 'true');
|
||||
}
|
||||
|
||||
// Background media
|
||||
if (backgroundPlayEnabled && NativeModules.BackgroundMedia && window.currentMediaInfo) {
|
||||
const { title, channel } = window.currentMediaInfo;
|
||||
NativeModules.BackgroundMedia.showPlaybackNotification(title, channel, null, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (AppState.currentState && AppState.currentState.match(/active/)) {
|
||||
// Cleanup blobs for completed files upon app resume to save space
|
||||
dispatch(doDeleteCompleteBlobs());
|
||||
if (backgroundPlayEnabled || NativeModules.BackgroundMedia) {
|
||||
NativeModules.BackgroundMedia.hidePlaybackNotification();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -299,6 +308,7 @@ class AppWithNavigationState extends React.Component {
|
|||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
backgroundPlayEnabled: makeSelectClientSetting(SETTINGS.BACKGROUND_PLAY_ENABLED)(state),
|
||||
keepDaemonRunning: makeSelectClientSetting(SETTINGS.KEEP_DAEMON_RUNNING)(state),
|
||||
nav: state.nav,
|
||||
notification: selectNotification(state),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import { Lbry } from 'lbry-redux';
|
||||
import {
|
||||
DeviceEventEmitter,
|
||||
NativeModules,
|
||||
PanResponder,
|
||||
Text,
|
||||
|
@ -225,9 +226,13 @@ class MediaPlayer extends React.PureComponent {
|
|||
|
||||
componentDidMount() {
|
||||
this.setSeekerPosition(this.calculateSeekerPosition());
|
||||
DeviceEventEmitter.addListener('onBackgroundPlayPressed', this.play);
|
||||
DeviceEventEmitter.addListener('onBackgroundPausePressed', this.pause);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
DeviceEventEmitter.removeListener('onBackgroundPlayPressed', this.play);
|
||||
DeviceEventEmitter.removeListener('onBackgroundPausePressed', this.pause);
|
||||
this.clearControlsTimeout();
|
||||
this.setState({ paused: true, fullscreenMode: false });
|
||||
const { onFullscreenToggled } = this.props;
|
||||
|
@ -236,6 +241,24 @@ class MediaPlayer extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
play = () => {
|
||||
this.setState({ paused: false }, this.updateBackgroundMediaNotification);
|
||||
}
|
||||
|
||||
pause = () => {
|
||||
this.setState({ paused: true }, this.updateBackgroundMediaNotification);
|
||||
}
|
||||
|
||||
updateBackgroundMediaNotification = () => {
|
||||
const { backgroundPlayEnabled } = this.props;
|
||||
if (backgroundPlayEnabled) {
|
||||
if (NativeModules.BackgroundMedia && window.currentMediaInfo) {
|
||||
const { title, channel } = window.currentMediaInfo;
|
||||
NativeModules.BackgroundMedia.showPlaybackNotification(title, channel, null, this.state.paused);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderPlayerControls() {
|
||||
if (this.state.areControlsVisible) {
|
||||
return (
|
||||
|
@ -285,7 +308,7 @@ class MediaPlayer extends React.PureComponent {
|
|||
return (
|
||||
<View style={styles} onLayout={onLayout}>
|
||||
<Video source={{ uri: 'file:///' + this.getEncodedDownloadPath(fileInfo) }}
|
||||
ref={(ref: Video) => { this.video = ref }}
|
||||
ref={(ref: Video) => { this.video = ref; }}
|
||||
resizeMode={this.state.resizeMode}
|
||||
playInBackground={backgroundPlayEnabled}
|
||||
style={mediaPlayerStyle.player}
|
||||
|
|
|
@ -152,6 +152,9 @@ class FilePage extends React.PureComponent {
|
|||
utility.keepAwakeOff();
|
||||
utility.showNavigationBar();
|
||||
}
|
||||
if (window.currentMediaInfo) {
|
||||
window.currentMediaInfo = null;
|
||||
}
|
||||
}
|
||||
|
||||
localUriForFileInfo = (fileInfo) => {
|
||||
|
@ -322,7 +325,13 @@ class FilePage extends React.PureComponent {
|
|||
style={playerStyle}
|
||||
autoPlay={this.state.autoPlayMedia}
|
||||
onFullscreenToggled={this.handleFullscreenToggle}
|
||||
onMediaLoaded={() => { this.setState({ mediaLoaded: true }); }}
|
||||
onMediaLoaded={() => {
|
||||
this.setState({ mediaLoaded: true });
|
||||
window.currentMediaInfo = {
|
||||
title: title,
|
||||
channel: channelName
|
||||
};
|
||||
}}
|
||||
onLayout={(evt) => {
|
||||
if (!this.state.playerHeight) {
|
||||
this.setState({ playerHeight: evt.nativeEvent.layout.height });
|
||||
|
|
|
@ -148,7 +148,7 @@ android.react_src = ./app
|
|||
|
||||
# (list) Gradle dependencies to add (currently works only with sdl2_gradle
|
||||
# bootstrap)
|
||||
android.gradle_dependencies = com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.55.3, com.mixpanel.android:mixpanel-android:5+, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0
|
||||
android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.55.3, com.mixpanel.android:mixpanel-android:5+, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0
|
||||
|
||||
# (str) python-for-android branch to use, defaults to master
|
||||
#p4a.branch = stable
|
||||
|
|
|
@ -148,7 +148,7 @@ android.react_src = ./app
|
|||
|
||||
# (list) Gradle dependencies to add (currently works only with sdl2_gradle
|
||||
# bootstrap)
|
||||
android.gradle_dependencies = com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.55.3, com.mixpanel.android:mixpanel-android:5+, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0
|
||||
android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.55.3, com.mixpanel.android:mixpanel-android:5+, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0
|
||||
|
||||
# (str) python-for-android branch to use, defaults to master
|
||||
#p4a.branch = stable
|
||||
|
|
|
@ -148,7 +148,7 @@ android.react_src = ./app
|
|||
|
||||
# (list) Gradle dependencies to add (currently works only with sdl2_gradle
|
||||
# bootstrap)
|
||||
android.gradle_dependencies = com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.55.3, com.mixpanel.android:mixpanel-android:5+, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0
|
||||
android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.55.3, com.mixpanel.android:mixpanel-android:5+, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0
|
||||
|
||||
# (str) python-for-android branch to use, defaults to master
|
||||
#p4a.branch = stable
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.os.Build;
|
|||
import android.os.Bundle;
|
||||
import android.app.Activity;
|
||||
import android.app.ActivityManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
|
@ -13,6 +14,7 @@ import android.Manifest;
|
|||
import android.net.Uri;
|
||||
import android.provider.Settings;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.app.NotificationManagerCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.widget.Toast;
|
||||
|
@ -22,10 +24,13 @@ import com.facebook.react.common.LifecycleState;
|
|||
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
|
||||
import com.facebook.react.ReactRootView;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
import com.facebook.react.shell.MainReactPackage;
|
||||
import com.RNFetchBlob.RNFetchBlobPackage;
|
||||
|
||||
import io.lbry.browser.reactpackages.LbryReactPackage;
|
||||
import io.lbry.browser.reactmodules.BackgroundMediaModule;
|
||||
import io.lbry.browser.reactmodules.DownloadManagerModule;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
@ -44,6 +49,8 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
|
|||
|
||||
private static final int PHONE_STATE_PERMISSION_REQ_CODE = 202;
|
||||
|
||||
private BroadcastReceiver backgroundMediaReceiver;
|
||||
|
||||
private ReactRootView mReactRootView;
|
||||
|
||||
private ReactInstanceManager mReactInstanceManager;
|
||||
|
@ -99,9 +106,36 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
|
|||
.build();
|
||||
mReactRootView.startReactApplication(mReactInstanceManager, "LBRYApp", null);
|
||||
|
||||
registerBackgroundMediaReceiver();
|
||||
|
||||
setContentView(mReactRootView);
|
||||
}
|
||||
|
||||
private void registerBackgroundMediaReceiver() {
|
||||
// Background media receiver
|
||||
IntentFilter backgroundMediaFilter = new IntentFilter();
|
||||
backgroundMediaFilter.addAction(BackgroundMediaModule.ACTION_PLAY);
|
||||
backgroundMediaFilter.addAction(BackgroundMediaModule.ACTION_PAUSE);
|
||||
backgroundMediaReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
ReactContext reactContext = mReactInstanceManager.getCurrentReactContext();
|
||||
if (reactContext != null) {
|
||||
if (BackgroundMediaModule.ACTION_PLAY.equals(action)) {
|
||||
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
.emit("onBackgroundPlayPressed", null);
|
||||
}
|
||||
if (BackgroundMediaModule.ACTION_PAUSE.equals(action)) {
|
||||
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
.emit("onBackgroundPausePressed", null);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
registerReceiver(backgroundMediaReceiver, backgroundMediaFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == OVERLAY_PERMISSION_REQ_CODE) {
|
||||
|
@ -230,6 +264,12 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
|
|||
}
|
||||
}
|
||||
|
||||
if (backgroundMediaReceiver != null) {
|
||||
unregisterReceiver(backgroundMediaReceiver);
|
||||
}
|
||||
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
|
||||
notificationManager.cancelAll();
|
||||
super.onDestroy();
|
||||
|
||||
if (mReactInstanceManager != null) {
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
package io.lbry.browser.reactmodules;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.NotificationManagerCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
|
||||
import io.lbry.browser.MainActivity;
|
||||
import io.lbry.browser.R;
|
||||
|
||||
public class BackgroundMediaModule extends ReactContextBaseJavaModule {
|
||||
|
||||
private static final int NOTIFICATION_ID = 900;
|
||||
|
||||
private static final String NOTIFICATION_CHANNEL_ID = "io.lbry.browser.MEDIA_PLAYER_NOTIFICATION_CHANNEL";
|
||||
|
||||
public static final String ACTION_PLAY = "io.lbry.browser.ACTION_MEDIA_PLAY";
|
||||
|
||||
I usually format this way to improve readability. Also helps when adding Javadocs. I usually format this way to improve readability. Also helps when adding Javadocs.
|
||||
public static final String ACTION_PAUSE = "io.lbry.browser.ACTION_MEDIA_PAUSE";
|
||||
|
||||
public static final String ACTION_STOP = "io.lbry.browser.ACTION_MEDIA_STOP";
|
||||
|
||||
private boolean channelCreated;
|
||||
|
||||
private Context context;
|
||||
|
||||
public BackgroundMediaModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
this.context = reactContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "BackgroundMedia";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void showPlaybackNotification(String title, String publisher, String uri, boolean paused) {
|
||||
if (!channelCreated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
NOTIFICATION_CHANNEL_ID, "LBRY Media", NotificationManager.IMPORTANCE_LOW);
|
||||
channel.setDescription("LBRY media player");
|
||||
Does this render within the notification? Does this render within the notification?
Maybe we should adjust this text a bit.
I'll need to play with this a bit on my device I'll need to play with this a bit on my device
No, it doesn't. Notification channels are usually configured in notification settings on Android 8.0 and higher. Basically, you can turn channels on or off which lets you determine the notifications related to channels that you want to see. https://developer.android.com/training/notify-user/channels No, it doesn't. Notification channels are usually configured in notification settings on Android 8.0 and higher. Basically, you can turn channels on or off which lets you determine the notifications related to channels that you want to see. https://developer.android.com/training/notify-user/channels
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
channelCreated = true;
|
||||
}
|
||||
|
||||
Intent playIntent = new Intent();
|
||||
playIntent.setAction(ACTION_PLAY);
|
||||
PendingIntent playPendingIntent = PendingIntent.getBroadcast(context, 0, playIntent, 0);
|
||||
|
||||
Intent pauseIntent = new Intent();
|
||||
pauseIntent.setAction(ACTION_PAUSE);
|
||||
PendingIntent pausePendingIntent = PendingIntent.getBroadcast(context, 0, pauseIntent, 0);
|
||||
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
|
||||
builder.setColor(ContextCompat.getColor(context, R.color.lbrygreen))
|
||||
.setContentTitle(title)
|
||||
.setContentText(publisher)
|
||||
.setOngoing(!paused)
|
||||
.setSmallIcon(paused ? android.R.drawable.ic_media_pause : android.R.drawable.ic_media_play)
|
||||
.setStyle(new android.support.v4.media.app.NotificationCompat.MediaStyle()
|
||||
.setShowActionsInCompactView(0))
|
||||
.addAction(paused ? android.R.drawable.ic_media_play : android.R.drawable.ic_media_pause,
|
||||
paused ? "Play" : "Pause",
|
||||
paused ? playPendingIntent : pausePendingIntent)
|
||||
.build();
|
||||
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build());
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void hidePlaybackNotification() {
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
notificationManager.cancel(NOTIFICATION_ID);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package io.lbry.browser.reactmodules;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.NotificationChannel;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.SharedPreferences;
|
||||
|
@ -14,6 +15,7 @@ import io.lbry.browser.MainActivity;
|
|||
import io.lbry.browser.ServiceHelper;
|
||||
|
||||
public class DaemonServiceControlModule extends ReactContextBaseJavaModule {
|
||||
|
||||
private Context context;
|
||||
|
||||
public DaemonServiceControlModule(ReactApplicationContext reactContext) {
|
||||
|
|
|
@ -5,6 +5,7 @@ import com.facebook.react.bridge.NativeModule;
|
|||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import io.lbry.browser.reactmodules.BackgroundMediaModule;
|
||||
import io.lbry.browser.reactmodules.DaemonServiceControlModule;
|
||||
import io.lbry.browser.reactmodules.DownloadManagerModule;
|
||||
import io.lbry.browser.reactmodules.FirstRunModule;
|
||||
|
@ -27,6 +28,7 @@ public class LbryReactPackage implements ReactPackage {
|
|||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
List<NativeModule> modules = new ArrayList<>();
|
||||
|
||||
modules.add(new BackgroundMediaModule(reactContext));
|
||||
modules.add(new DaemonServiceControlModule(reactContext));
|
||||
modules.add(new DownloadManagerModule(reactContext));
|
||||
modules.add(new FirstRunModule(reactContext));
|
||||
|
|
Loading…
Add table
Reference in a new issue
Some of these line breaks aren't needed here