Background media controls #242

Merged
akinwale merged 2 commits from background-media-controls into master 2018-08-20 17:00:17 +02:00
10 changed files with 183 additions and 6 deletions

View file

@ -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),

View file

@ -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}

View file

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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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) {

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";
skhameneh commented 2018-08-20 16:44:41 +02:00 (Migrated from github.com)
Review

Some of these line breaks aren't needed here

Some of these line breaks aren't needed here
akinwale commented 2018-08-20 16:59:55 +02:00 (Migrated from github.com)
Review

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");
skhameneh commented 2018-08-20 16:46:43 +02:00 (Migrated from github.com)
Review

Does this render within the notification?
Maybe we should adjust this text a bit.

Does this render within the notification? Maybe we should adjust this text a bit.
skhameneh commented 2018-08-20 16:47:24 +02:00 (Migrated from github.com)
Review

I'll need to play with this a bit on my device

I'll need to play with this a bit on my device
akinwale commented 2018-08-20 16:56:54 +02:00 (Migrated from github.com)
Review

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

View file

@ -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) {

View file

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