diff --git a/android/app/build.gradle b/android/app/build.gradle
index 9c9cf2c8..28213273 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -13,8 +13,8 @@ android {
applicationId "com.audiobookshelf.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 46
- versionName "0.9.27-beta"
+ versionCode 47
+ versionName "0.9.28-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
diff --git a/android/app/src/main/java/com/audiobookshelf/app/AudiobookManager.kt b/android/app/src/main/java/com/audiobookshelf/app/AudiobookManager.kt
index 764314ac..c1a8bddc 100644
--- a/android/app/src/main/java/com/audiobookshelf/app/AudiobookManager.kt
+++ b/android/app/src/main/java/com/audiobookshelf/app/AudiobookManager.kt
@@ -132,7 +132,7 @@ class AudiobookManager {
}
fun openStream(audiobook:Audiobook, streamListener:OnStreamData) {
- var url = "$serverUrl/api/audiobook/${audiobook.id}/stream"
+ var url = "$serverUrl/api/books/${audiobook.id}/stream"
val request = Request.Builder()
.url(url).addHeader("Authorization", "Bearer $token")
.build()
diff --git a/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt b/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt
index 435d2931..3f97f0fa 100644
--- a/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt
+++ b/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt
@@ -5,6 +5,8 @@ import android.app.DownloadManager
import android.app.SearchManager
import android.content.*
import android.content.pm.PackageManager
+import android.hardware.Sensor
+import android.hardware.SensorManager
import android.os.*
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
@@ -27,6 +29,11 @@ class MainActivity : BridgeActivity() {
val storageHelper = SimpleStorageHelper(this)
val storage = SimpleStorage(this)
+ // The following are used for the shake detection
+ private var mSensorManager: SensorManager? = null
+ private var mAccelerometer: Sensor? = null
+ private var mShakeDetector: ShakeDetector? = null
+
val broadcastReceiver = object: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
@@ -54,6 +61,24 @@ class MainActivity : BridgeActivity() {
addAction(DownloadManager.ACTION_NOTIFICATION_CLICKED)
}
registerReceiver(broadcastReceiver, filter)
+
+ initSensor()
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ Log.d(tag, "onResume Register sensor listener")
+ mSensorManager!!.registerListener(
+ mShakeDetector,
+ mAccelerometer,
+ SensorManager.SENSOR_DELAY_UI
+ )
+ }
+
+ override fun onPause() {
+ mSensorManager!!.unregisterListener(mShakeDetector)
+ super.onPause()
}
override fun onDestroy() {
@@ -128,6 +153,19 @@ class MainActivity : BridgeActivity() {
storageHelper.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
+ private fun initSensor() {
+ // ShakeDetector initialization
+ mSensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
+ mAccelerometer = mSensorManager!!.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
+ mShakeDetector = ShakeDetector()
+ mShakeDetector!!.setOnShakeListener(object : ShakeDetector.OnShakeListener {
+ override fun onShake(count: Int) {
+ Log.d(tag, "PHONE SHAKE! $count")
+ foregroundService.handleShake()
+ }
+ })
+ }
+
// override fun onUserInteraction() {
// super.onUserInteraction()
// Log.d(tag, "USER INTERACTION")
diff --git a/android/app/src/main/java/com/audiobookshelf/app/MyNativeAudio.kt b/android/app/src/main/java/com/audiobookshelf/app/MyNativeAudio.kt
index 3a199533..551024ba 100644
--- a/android/app/src/main/java/com/audiobookshelf/app/MyNativeAudio.kt
+++ b/android/app/src/main/java/com/audiobookshelf/app/MyNativeAudio.kt
@@ -38,6 +38,9 @@ class MyNativeAudio : Plugin() {
override fun onSleepTimerEnded(currentPosition:Long) {
emit("onSleepTimerEnded", currentPosition)
}
+ override fun onSleepTimerSet(sleepTimerEndTime:Long) {
+ emit("onSleepTimerSet", sleepTimerEndTime)
+ }
})
}
mainActivity.pluginCallback = foregroundServiceReady
@@ -81,7 +84,6 @@ class MyNativeAudio : Plugin() {
fun getCurrentTime(call: PluginCall) {
Handler(Looper.getMainLooper()).post() {
var currentTime = playerNotificationService.getCurrentTime()
- Log.d(tag, "Get Current Time $currentTime")
val ret = JSObject()
ret.put("value", currentTime)
call.resolve(ret)
@@ -95,7 +97,6 @@ class MyNativeAudio : Plugin() {
var lastPauseTime = playerNotificationService.getTheLastPauseTime()
Log.d(tag, "Get Last Pause Time $lastPauseTime")
var currentTime = playerNotificationService.getCurrentTime()
- Log.d(tag, "Get Current Time $currentTime")
//if (!isPlaying) currentTime -= playerNotificationService.calcPauseSeekBackTime()
var id = playerNotificationService.getCurrentAudiobookId()
Log.d(tag, "Get Current id $id")
diff --git a/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt
index 48647965..b66ab10b 100644
--- a/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt
+++ b/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt
@@ -49,9 +49,11 @@ import okhttp3.OkHttpClient
import org.json.JSONObject
import java.util.*
import kotlin.concurrent.schedule
+import kotlin.math.roundToInt
const val NOTIFICATION_LARGE_ICON_SIZE = 144 // px
+const val SLEEP_EXTENSION_TIME = 900000L // 15m
class PlayerNotificationService : MediaBrowserServiceCompat() {
@@ -64,6 +66,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
fun onMetadata(metadata: JSObject)
fun onPrepare(audiobookId: String, playWhenReady: Boolean)
fun onSleepTimerEnded(currentPosition: Long)
+ fun onSleepTimerSet(sleepTimerEndTime:Long)
}
@@ -101,7 +104,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
private var onSeekBack: Boolean = false
private var sleepTimerTask:TimerTask? = null
- private var sleepChapterTime:Long = 0L
+ private var sleepTimerRunning:Boolean = false
+ private var sleepTimerEndTime:Long = 0L
private lateinit var audiobookManager:AudiobookManager
private var newConnectionListener:SessionListener? = null
@@ -490,7 +494,12 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
handleMediaButtonClickCount()
}
KeyEvent.KEYCODE_MEDIA_PLAY -> {
- if (0 == mediaButtonClickCount) play()
+ if (0 == mediaButtonClickCount) {
+ play()
+ if (sleepTimerRunning) {
+ extendSleepTime()
+ }
+ }
handleMediaButtonClickCount()
}
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
@@ -511,7 +520,12 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
if (0 == mediaButtonClickCount) pause()
handleMediaButtonClickCount()
} else {
- if (0 == mediaButtonClickCount) play()
+ if (0 == mediaButtonClickCount) {
+ play()
+ if (sleepTimerRunning) {
+ extendSleepTime()
+ }
+ }
handleMediaButtonClickCount()
}
}
@@ -759,7 +773,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
fun getCurrentTime() : Long {
- return mPlayer.currentPosition
+ return currentPlayer.currentPosition
}
fun getTheLastPauseTime() : Long {
@@ -767,7 +781,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
}
fun getDuration() : Long {
- return mPlayer.duration
+ return currentPlayer.duration
}
fun calcPauseSeekBackTime() : Long {
@@ -989,57 +1003,89 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
fun setSleepTimer(time: Long, isChapterTime: Boolean) : Boolean {
Log.d(tag, "Setting Sleep Timer for $time is chapter time $isChapterTime")
sleepTimerTask?.cancel()
- sleepChapterTime = 0L
+ sleepTimerRunning = false
+ var currentTime = getCurrentTime()
if (isChapterTime) {
- // Validate time
- if (currentPlayer.isPlaying) {
- if (currentPlayer.currentPosition >= time) {
- Log.d(tag, "Invalid setSleepTimer chapter time is already passed")
- return false
- }
+ if (currentTime > time) {
+ Log.d(tag, "Invalid sleep timer - current time is already passed chapter time $time")
+ return false
}
+ sleepTimerEndTime = time
+ } else {
+ sleepTimerEndTime = currentTime + time
+ }
- sleepChapterTime = time
- sleepTimerTask = Timer("SleepTimer", false).schedule(0L, 1000L) {
- Handler(Looper.getMainLooper()).post() {
- if (currentPlayer.isPlaying && currentPlayer.currentPosition > sleepChapterTime) {
+ if (sleepTimerEndTime > getDuration()) {
+ sleepTimerEndTime = getDuration()
+ }
+
+ Log.d(tag, "SLEEP VOLUME ${currentPlayer.volume} | ${currentPlayer.deviceVolume}")
+// if (isChapterTime) {
+// sleepChapterTime = time
+ listener?.onSleepTimerSet(sleepTimerEndTime)
+
+ sleepTimerRunning = true
+ sleepTimerTask = Timer("SleepTimer", false).schedule(0L, 1000L) {
+ Handler(Looper.getMainLooper()).post() {
+ if (currentPlayer.isPlaying) {
+ var sleepTimeRemaining = sleepTimerEndTime - getCurrentTime()
+ var sleepTimeSecondsRemaining = ((sleepTimeRemaining / 1000).toDouble()).roundToInt()
+ Log.d(tag, "Sleep TIMER time remaining $sleepTimeSecondsRemaining s")
+
+ if (sleepTimeRemaining <= 0) {
Log.d(tag, "Sleep Timer Pausing Player on Chapter")
currentPlayer.pause()
listener?.onSleepTimerEnded(currentPlayer.currentPosition)
sleepTimerTask?.cancel()
+ sleepTimerRunning = false
+ } else if (sleepTimeSecondsRemaining <= 30) {
+ // Start fading out audio
+ var volume = sleepTimeSecondsRemaining / 30F
+ Log.d(tag, "SLEEP VOLUME FADE $volume | ${sleepTimeSecondsRemaining}s remaining")
+ currentPlayer.volume = volume
}
}
}
- } else {
- sleepTimerTask = Timer("SleepTimer", false).schedule(time) {
- Log.d(tag, "Sleep Timer Done")
- Handler(Looper.getMainLooper()).post() {
- if (currentPlayer.isPlaying) {
- Log.d(tag, "Sleep Timer Pausing Player")
- currentPlayer.pause()
- }
- listener?.onSleepTimerEnded(currentPlayer.currentPosition)
- }
- }
}
return true
}
fun getSleepTimerTime():Long? {
- var time = sleepTimerTask?.scheduledExecutionTime()
- Log.d(tag, "Sleep Timer execution time $time")
- return time
+ return sleepTimerEndTime
}
fun cancelSleepTimer() {
Log.d(tag, "Canceling Sleep Timer")
sleepTimerTask?.cancel()
sleepTimerTask = null
- sleepChapterTime = 0L
+ sleepTimerEndTime = 0
+ sleepTimerRunning = false
+ listener?.onSleepTimerSet(0)
}
+ private fun extendSleepTime() {
+ if (!sleepTimerRunning) return
+ currentPlayer.volume = 1F
+ sleepTimerEndTime += SLEEP_EXTENSION_TIME
+ if (sleepTimerEndTime > getDuration()) sleepTimerEndTime = getDuration()
+ listener?.onSleepTimerSet(sleepTimerEndTime)
+ }
+
+ fun handleShake() {
+ Log.d(tag, "HANDLE SHAKE HERE")
+ if (sleepTimerRunning) {
+ Log.d(tag, "Sleep Timer is Running, EXTEND TIME")
+ extendSleepTime()
+ }
+ }
+
+
+ /*
+ CAST STUFF
+ */
+
private inner class CastSessionAvailabilityListener : SessionAvailabilityListener {
/**
diff --git a/android/app/src/main/java/com/audiobookshelf/app/ShakeDetector.kt b/android/app/src/main/java/com/audiobookshelf/app/ShakeDetector.kt
new file mode 100644
index 00000000..ac2b31b9
--- /dev/null
+++ b/android/app/src/main/java/com/audiobookshelf/app/ShakeDetector.kt
@@ -0,0 +1,67 @@
+package com.audiobookshelf.app
+
+import android.hardware.Sensor
+import android.hardware.SensorEvent
+import android.hardware.SensorEventListener
+import android.hardware.SensorManager
+import java.lang.Math.sqrt
+import kotlin.math.sqrt
+
+class ShakeDetector : SensorEventListener {
+ private var mListener: OnShakeListener? = null
+ private var mShakeTimestamp: Long = 0
+ private var mShakeCount = 0
+ fun setOnShakeListener(listener: OnShakeListener?) {
+ mListener = listener
+ }
+
+ interface OnShakeListener {
+ fun onShake(count: Int)
+ }
+
+ override fun onAccuracyChanged(
+ sensor: Sensor,
+ accuracy: Int
+ ) { // ignore
+ }
+
+ override fun onSensorChanged(event: SensorEvent) {
+ if (mListener != null) {
+ val x = event.values[0]
+ val y = event.values[1]
+ val z = event.values[2]
+ val gX = x / SensorManager.GRAVITY_EARTH
+ val gY = y / SensorManager.GRAVITY_EARTH
+ val gZ = z / SensorManager.GRAVITY_EARTH
+ // gForce will be close to 1 when there is no movement.
+ val gForce: Float = sqrt(gX * gX + gY * gY + gZ * gZ)
+ if (gForce > SHAKE_THRESHOLD_GRAVITY) {
+ val now = System.currentTimeMillis()
+ // ignore shake events too close to each other (500ms)
+ if (mShakeTimestamp + SHAKE_SLOP_TIME_MS > now) {
+ return
+ }
+ // reset the shake count after 3 seconds of no shakes
+ if (mShakeTimestamp + SHAKE_COUNT_RESET_TIME_MS < now) {
+ mShakeCount = 0
+ }
+ mShakeTimestamp = now
+ mShakeCount++
+ mListener!!.onShake(mShakeCount)
+ }
+ }
+ }
+
+ companion object {
+ /*
+ * The gForce that is necessary to register as shake.
+ * Must be greater than 1G (one earth gravity unit).
+ * You can install "G-Force", by Blake La Pierre
+ * from the Google Play Store and run it to see how
+ * many G's it takes to register a shake
+ */
+ private const val SHAKE_THRESHOLD_GRAVITY = 2.7f
+ private const val SHAKE_SLOP_TIME_MS = 500
+ private const val SHAKE_COUNT_RESET_TIME_MS = 3000
+ }
+}
diff --git a/components/app/AudioPlayer.vue b/components/app/AudioPlayer.vue
index 93ee40be..a271c38e 100644
--- a/components/app/AudioPlayer.vue
+++ b/components/app/AudioPlayer.vue
@@ -32,8 +32,7 @@
-{{ $secondsToTimestamp(timeLeftInChapter) }}
-{{ Math.ceil(sleepTimeoutCurrentTime / 1000 / 60) }}m
+{{ sleepTimeRemainingPretty }}
0:00
+0:00
{{ currentChapterTitle }}
-{{ totalDurationPretty }}
+{{ timeRemainingPretty }}
EOC: {{ endOfChapterTimePretty }}
-{{ timeRemainingPretty }}
+{{ timeRemainingPretty }}