Background media controls (#242)

* create background control notification when background play is enabled
This commit is contained in:
Akinwale Ariwodola 2018-08-20 16:00:16 +01:00 committed by GitHub
parent da60aeb71b
commit df561cc582
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 183 additions and 6 deletions

View file

@ -228,7 +228,7 @@ class AppWithNavigationState extends React.Component {
} }
_handleAppStateChange = (nextAppState) => { _handleAppStateChange = (nextAppState) => {
const { dispatch } = this.props; const { backgroundPlayEnabled, dispatch } = this.props;
// Check if the app was suspended // Check if the app was suspended
if (AppState.currentState && AppState.currentState.match(/inactive|background/)) { if (AppState.currentState && AppState.currentState.match(/inactive|background/)) {
AsyncStorage.getItem('firstLaunchTime').then(start => { 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 // If so, this needs to be included as a property when tracking
AsyncStorage.setItem('firstLaunchSuspended', 'true'); 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/)) { if (AppState.currentState && AppState.currentState.match(/active/)) {
// Cleanup blobs for completed files upon app resume to save space // Cleanup blobs for completed files upon app resume to save space
dispatch(doDeleteCompleteBlobs()); dispatch(doDeleteCompleteBlobs());
if (backgroundPlayEnabled || NativeModules.BackgroundMedia) {
NativeModules.BackgroundMedia.hidePlaybackNotification();
}
} }
} }
@ -299,6 +308,7 @@ class AppWithNavigationState extends React.Component {
} }
const mapStateToProps = state => ({ const mapStateToProps = state => ({
backgroundPlayEnabled: makeSelectClientSetting(SETTINGS.BACKGROUND_PLAY_ENABLED)(state),
keepDaemonRunning: makeSelectClientSetting(SETTINGS.KEEP_DAEMON_RUNNING)(state), keepDaemonRunning: makeSelectClientSetting(SETTINGS.KEEP_DAEMON_RUNNING)(state),
nav: state.nav, nav: state.nav,
notification: selectNotification(state), notification: selectNotification(state),

View file

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { Lbry } from 'lbry-redux'; import { Lbry } from 'lbry-redux';
import { import {
DeviceEventEmitter,
NativeModules, NativeModules,
PanResponder, PanResponder,
Text, Text,
@ -225,9 +226,13 @@ class MediaPlayer extends React.PureComponent {
componentDidMount() { componentDidMount() {
this.setSeekerPosition(this.calculateSeekerPosition()); this.setSeekerPosition(this.calculateSeekerPosition());
DeviceEventEmitter.addListener('onBackgroundPlayPressed', this.play);
DeviceEventEmitter.addListener('onBackgroundPausePressed', this.pause);
} }
componentWillUnmount() { componentWillUnmount() {
DeviceEventEmitter.removeListener('onBackgroundPlayPressed', this.play);
DeviceEventEmitter.removeListener('onBackgroundPausePressed', this.pause);
this.clearControlsTimeout(); this.clearControlsTimeout();
this.setState({ paused: true, fullscreenMode: false }); this.setState({ paused: true, fullscreenMode: false });
const { onFullscreenToggled } = this.props; 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() { renderPlayerControls() {
if (this.state.areControlsVisible) { if (this.state.areControlsVisible) {
return ( return (
@ -285,7 +308,7 @@ class MediaPlayer extends React.PureComponent {
return ( return (
<View style={styles} onLayout={onLayout}> <View style={styles} onLayout={onLayout}>
<Video source={{ uri: 'file:///' + this.getEncodedDownloadPath(fileInfo) }} <Video source={{ uri: 'file:///' + this.getEncodedDownloadPath(fileInfo) }}
ref={(ref: Video) => { this.video = ref }} ref={(ref: Video) => { this.video = ref; }}
resizeMode={this.state.resizeMode} resizeMode={this.state.resizeMode}
playInBackground={backgroundPlayEnabled} playInBackground={backgroundPlayEnabled}
style={mediaPlayerStyle.player} style={mediaPlayerStyle.player}

View file

@ -152,6 +152,9 @@ class FilePage extends React.PureComponent {
utility.keepAwakeOff(); utility.keepAwakeOff();
utility.showNavigationBar(); utility.showNavigationBar();
} }
if (window.currentMediaInfo) {
window.currentMediaInfo = null;
}
} }
localUriForFileInfo = (fileInfo) => { localUriForFileInfo = (fileInfo) => {
@ -322,7 +325,13 @@ class FilePage extends React.PureComponent {
style={playerStyle} style={playerStyle}
autoPlay={this.state.autoPlayMedia} autoPlay={this.state.autoPlayMedia}
onFullscreenToggled={this.handleFullscreenToggle} onFullscreenToggled={this.handleFullscreenToggle}
onMediaLoaded={() => { this.setState({ mediaLoaded: true }); }} onMediaLoaded={() => {
this.setState({ mediaLoaded: true });
window.currentMediaInfo = {
title: title,
channel: channelName
};
}}
onLayout={(evt) => { onLayout={(evt) => {
if (!this.state.playerHeight) { if (!this.state.playerHeight) {
this.setState({ playerHeight: evt.nativeEvent.layout.height }); this.setState({ playerHeight: evt.nativeEvent.layout.height });

View file

@ -148,7 +148,7 @@ android.react_src = ./app
# (list) Gradle dependencies to add (currently works only with sdl2_gradle # (list) Gradle dependencies to add (currently works only with sdl2_gradle
# bootstrap) # 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 # (str) python-for-android branch to use, defaults to master
#p4a.branch = stable #p4a.branch = stable

View file

@ -148,7 +148,7 @@ android.react_src = ./app
# (list) Gradle dependencies to add (currently works only with sdl2_gradle # (list) Gradle dependencies to add (currently works only with sdl2_gradle
# bootstrap) # 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 # (str) python-for-android branch to use, defaults to master
#p4a.branch = stable #p4a.branch = stable

View file

@ -148,7 +148,7 @@ android.react_src = ./app
# (list) Gradle dependencies to add (currently works only with sdl2_gradle # (list) Gradle dependencies to add (currently works only with sdl2_gradle
# bootstrap) # 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 # (str) python-for-android branch to use, defaults to master
#p4a.branch = stable #p4a.branch = stable

View file

@ -4,6 +4,7 @@ import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.app.Activity; import android.app.Activity;
import android.app.ActivityManager; import android.app.ActivityManager;
import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
@ -13,6 +14,7 @@ import android.Manifest;
import android.net.Uri; import android.net.Uri;
import android.provider.Settings; import android.provider.Settings;
import android.support.v4.app.ActivityCompat; import android.support.v4.app.ActivityCompat;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.telephony.TelephonyManager; import android.telephony.TelephonyManager;
import android.widget.Toast; 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.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.ReactRootView; import com.facebook.react.ReactRootView;
import com.facebook.react.ReactInstanceManager; 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.facebook.react.shell.MainReactPackage;
import com.RNFetchBlob.RNFetchBlobPackage; import com.RNFetchBlob.RNFetchBlobPackage;
import io.lbry.browser.reactpackages.LbryReactPackage; import io.lbry.browser.reactpackages.LbryReactPackage;
import io.lbry.browser.reactmodules.BackgroundMediaModule;
import io.lbry.browser.reactmodules.DownloadManagerModule; import io.lbry.browser.reactmodules.DownloadManagerModule;
import java.io.UnsupportedEncodingException; 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 static final int PHONE_STATE_PERMISSION_REQ_CODE = 202;
private BroadcastReceiver backgroundMediaReceiver;
private ReactRootView mReactRootView; private ReactRootView mReactRootView;
private ReactInstanceManager mReactInstanceManager; private ReactInstanceManager mReactInstanceManager;
@ -99,9 +106,36 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
.build(); .build();
mReactRootView.startReactApplication(mReactInstanceManager, "LBRYApp", null); mReactRootView.startReactApplication(mReactInstanceManager, "LBRYApp", null);
registerBackgroundMediaReceiver();
setContentView(mReactRootView); 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 @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == OVERLAY_PERMISSION_REQ_CODE) { 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(); super.onDestroy();
if (mReactInstanceManager != null) { if (mReactInstanceManager != null) {

View file

@ -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";
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");
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);
}
}

View file

@ -1,6 +1,7 @@
package io.lbry.browser.reactmodules; package io.lbry.browser.reactmodules;
import android.app.Activity; import android.app.Activity;
import android.app.NotificationChannel;
import android.content.Context; import android.content.Context;
import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo;
import android.content.SharedPreferences; import android.content.SharedPreferences;
@ -14,6 +15,7 @@ import io.lbry.browser.MainActivity;
import io.lbry.browser.ServiceHelper; import io.lbry.browser.ServiceHelper;
public class DaemonServiceControlModule extends ReactContextBaseJavaModule { public class DaemonServiceControlModule extends ReactContextBaseJavaModule {
private Context context; private Context context;
public DaemonServiceControlModule(ReactApplicationContext reactContext) { public DaemonServiceControlModule(ReactApplicationContext reactContext) {

View file

@ -5,6 +5,7 @@ import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager; import com.facebook.react.uimanager.ViewManager;
import io.lbry.browser.reactmodules.BackgroundMediaModule;
import io.lbry.browser.reactmodules.DaemonServiceControlModule; import io.lbry.browser.reactmodules.DaemonServiceControlModule;
import io.lbry.browser.reactmodules.DownloadManagerModule; import io.lbry.browser.reactmodules.DownloadManagerModule;
import io.lbry.browser.reactmodules.FirstRunModule; import io.lbry.browser.reactmodules.FirstRunModule;
@ -27,6 +28,7 @@ public class LbryReactPackage implements ReactPackage {
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) { public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>(); List<NativeModule> modules = new ArrayList<>();
modules.add(new BackgroundMediaModule(reactContext));
modules.add(new DaemonServiceControlModule(reactContext)); modules.add(new DaemonServiceControlModule(reactContext));
modules.add(new DownloadManagerModule(reactContext)); modules.add(new DownloadManagerModule(reactContext));
modules.add(new FirstRunModule(reactContext)); modules.add(new FirstRunModule(reactContext));