Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[AV][Android] Fix AV progress update #7193

Merged
merged 7 commits into from Apr 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -16,6 +16,7 @@ This is the log of notable changes to the Expo client that are developer-facing.

### 🐛 Bug fixes

- Fixed multiplied callbacks in `expo-av` after replaying ([#7193](https://github.com/expo/expo/pull/7193) by [@mczernek](https://github.com/mczernek))
- Fixed `Brightness.requestPermissionsAsync` throwing `permission cannot be null or empty` error on Android. ([#7276](https://github.com/expo/expo/pull/7276) by [@lukmccall](https://github.com/lukmccall))
- Fixed `KeepAwake.activateKeepAwake` not working with multiple tags on Android. ([#7197](https://github.com/expo/expo/pull/7197) by [@lukmccall](https://github.com/lukmccall))
- Fix `Contacts.presentFormAsync` pre-filling. ([#7285](https://github.com/expo/expo/pull/7285) by [@abdelilah](https://github.com/abdelilah) & [@lukmccall](https://github.com/lukmccall))
Expand Down
3 changes: 1 addition & 2 deletions packages/@unimodules/core/unimodules-core.gradle
Expand Up @@ -5,7 +5,6 @@ class UnimodulesPlugin implements Plugin<Project> {
project.android.sourceSets {
main {
java {
srcDir 'src'
exclude '**/flutter/**'
}
}
Expand All @@ -22,7 +21,7 @@ class UnimodulesPlugin implements Plugin<Project> {
} else {
// There's no package.json and no pubspec.yaml
throw new GradleException(
"'unimodules-core.gradle' used in a project that seems to be neither a Flutter nor a React Native project."
"'unimodules-core.gradle' used in a project that seems not to be React Native project."
)
}

Expand Down
29 changes: 26 additions & 3 deletions packages/expo-av/android/build.gradle
@@ -1,12 +1,23 @@
apply plugin: 'com.android.library'
apply plugin: 'maven'
apply plugin: 'kotlin-android'

group = 'host.exp.exponent'
version = '8.1.0'

// Simple helper that allows the root project to override versions declared by this library.
def safeExtGet(prop, fallback) {
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
buildscript {
// Simple helper that allows the root project to override versions declared by this library.
ext.safeExtGet = { prop, fallback ->
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}

repositories {
mavenCentral()
}

dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${safeExtGet("kotlinVersion", "1.3.50")}")
}
}

// Upload android library to maven with javadoc and android sources
Expand Down Expand Up @@ -50,6 +61,13 @@ android {
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
}

testOptions {
unitTests.all {
useJUnitPlatform()
}
}

}

if (new File(rootProject.projectDir.parentFile, 'package.json').exists()) {
Expand All @@ -75,4 +93,9 @@ dependencies {
api "com.squareup.okhttp3:okhttp-urlconnection:3.10.0"

api "com.android.support:support-annotations:${safeExtGet("supportLibVersion", "28.0.0")}"

testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.1'
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.5.1"

testImplementation 'io.mockk:mockk:1.9'
}
Expand Up @@ -3,18 +3,19 @@
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.util.Pair;
import android.view.Surface;

import java.lang.ref.WeakReference;
import java.util.Map;

import org.unimodules.core.Promise;
import org.unimodules.core.arguments.ReadableArguments;

import java.util.Map;

import expo.modules.av.AVManagerInterface;
import expo.modules.av.AudioEventHandler;
import expo.modules.av.AudioFocusNotAcquiredException;
import expo.modules.av.progress.AndroidLooperTimeMachine;
import expo.modules.av.progress.ProgressLooper;

public abstract class PlayerData implements AudioEventHandler {
static final String STATUS_ANDROID_IMPLEMENTATION_KEY_PATH = "androidImplementation";
Expand Down Expand Up @@ -76,25 +77,8 @@ public interface FullscreenPresenter {
final Uri mUri;
final Map<String, Object> mRequestHeaders;

private Handler mHandler = new Handler();
private Runnable mProgressUpdater = new ProgressUpdater(this);

private class ProgressUpdater implements Runnable {
private WeakReference<PlayerData> mPlayerDataWeakReference;
private ProgressLooper mProgressUpdater = new ProgressLooper(new AndroidLooperTimeMachine());

private ProgressUpdater(PlayerData playerData) {
mPlayerDataWeakReference = new WeakReference<>(playerData);
}

@Override
public void run() {
final PlayerData playerData = mPlayerDataWeakReference.get();
if (playerData != null) {
playerData.callStatusUpdateListener();
playerData.progressUpdateLoop();
}
}
}

private FullscreenPresenter mFullscreenPresenter = null;
private StatusUpdateListener mStatusUpdateListener = null;
Expand Down Expand Up @@ -125,7 +109,7 @@ public static PlayerData createUnloadedPlayerData(final AVManagerInterface avMod
final Uri uri = Uri.parse(uriString);

if (status.containsKey(STATUS_ANDROID_IMPLEMENTATION_KEY_PATH)
&& status.getString(STATUS_ANDROID_IMPLEMENTATION_KEY_PATH).equals(MediaPlayerData.IMPLEMENTATION_NAME)) {
&& status.getString(STATUS_ANDROID_IMPLEMENTATION_KEY_PATH).equals(MediaPlayerData.IMPLEMENTATION_NAME)) {
return new MediaPlayerData(avModule, context, uri, requestHeaders);
} else {
return new SimpleExoPlayerData(avModule, context, uri, uriOverridingExtension, requestHeaders);
Expand Down Expand Up @@ -161,19 +145,25 @@ final void callStatusUpdateListener() {
abstract boolean shouldContinueUpdatingProgress();

final void stopUpdatingProgressIfNecessary() {
mHandler.removeCallbacks(mProgressUpdater);
mProgressUpdater.stopLooping();
}

private void progressUpdateLoop() {
if (!shouldContinueUpdatingProgress()) {
stopUpdatingProgressIfNecessary();
} else {
mHandler.postDelayed(mProgressUpdater, mProgressUpdateIntervalMillis);
mProgressUpdater.loop(mProgressUpdateIntervalMillis, () -> {
this.callStatusUpdateListener();
return null;
});
}
}

final void beginUpdatingProgressIfNecessary() {
mHandler.post(mProgressUpdater);
mProgressUpdater.loop(mProgressUpdateIntervalMillis, () -> {
this.callStatusUpdateListener();
return null;
});
}

public final void setStatusUpdateListener(final StatusUpdateListener listener) {
Expand All @@ -198,7 +188,7 @@ final boolean shouldPlayerPlay() {
abstract void playPlayerWithRateAndMuteIfNecessary() throws AudioFocusNotAcquiredException;

abstract void applyNewStatus(final Integer newPositionMillis, final Boolean newIsLooping)
throws AudioFocusNotAcquiredException, IllegalStateException;
throws AudioFocusNotAcquiredException, IllegalStateException;

final void setStatusWithListener(final Bundle status, final SetStatusCompletionListener setStatusCompletionListener) {
if (status.containsKey(STATUS_PROGRESS_UPDATE_INTERVAL_MILLIS_KEY_PATH)) {
Expand Down
@@ -0,0 +1,13 @@
package expo.modules.av.progress

import android.os.Handler

class AndroidLooperTimeMachine : TimeMachine {

override fun scheduleAt(intervalMillis: Long, callback: TimeMachineTick) {
Handler().postDelayed(callback, intervalMillis)
}

override val time: Long
get() = System.currentTimeMillis()
}
@@ -0,0 +1,67 @@
package expo.modules.av.progress

typealias TimeMachineTick = () -> Unit

interface TimeMachine {
bbarthec marked this conversation as resolved.
Show resolved Hide resolved
fun scheduleAt(intervalMillis: Long, callback: TimeMachineTick)
bbarthec marked this conversation as resolved.
Show resolved Hide resolved
val time: Long
}

typealias PlayerProgressListener = () -> Unit

class ProgressLooper(private val timeMachine: TimeMachine) {

private var interval = 0L
private var nextExpectedTick = -1L
private var waiting = false

private var shouldLoop: Boolean
get() = interval > 0 && nextExpectedTick >= 0 && !waiting
set(value) {
if (!value) {
interval = 0L
nextExpectedTick = -1L
waiting = false
}
}

private var listener: PlayerProgressListener? = null

fun setListener(listener: PlayerProgressListener) {
this.listener = listener
}

fun loop(interval: Long, listener: PlayerProgressListener) {
this.listener = listener
this.interval = interval
scheduleNextTick()
}

fun stopLooping() {
this.shouldLoop = false
this.listener = null
}

private fun scheduleNextTick() {
if (nextExpectedTick == -1L) {
nextExpectedTick = timeMachine.time
}
if (shouldLoop) {
nextExpectedTick += calculateNextInterval()
waiting = true
timeMachine.scheduleAt(nextExpectedTick - timeMachine.time) {
waiting = false
listener?.invoke()
scheduleNextTick()
}
}
}

private fun calculateNextInterval() =
if (nextExpectedTick > timeMachine.time) {
interval
} else {
(((timeMachine.time - nextExpectedTick) / interval) + 1) * interval
}

}