Commit 6248edd8 authored by Daniel Sonck's avatar Daniel Sonck
Browse files

Merge pull request #41 in THFM/fm.touhou.touhoufm from experimental/kotlin to development

* commit '080b9c43':
  [THFMA-27] [THFMA-24] Finish widget code for first test
  [THFMA-27] [THFMA-25] Add initial widget and new navigation
parents 78eba9c7 080b9c43
......@@ -18,8 +18,9 @@ plugins {
}
apply from: 'version.gradle'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'androidx.navigation.safeargs.kotlin'
if(project.hasProperty("TouHouFM.signing")
&& new File(project.property("TouHouFM.signing") + ".gradle").exists()) {
......@@ -40,15 +41,24 @@ dependencies {
testImplementation 'org.powermock:powermock-module-junit4:1.6.5'
testImplementation 'org.powermock:powermock-api-mockito:1.6.5'
implementation "com.android.support:support-v4:28.0.0"
implementation "com.android.support:support-v13:28.0.0"
implementation "com.android.support:cardview-v7:28.0.0"
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'org.java-websocket:Java-WebSocket:1.3.7'
implementation 'com.facebook.fresco:fresco:1.8.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "com.google.android.material:material:$rootProject.materialVersion"
implementation "androidx.appcompat:appcompat:$rootProject.appCompatVersion"
implementation "androidx.media:media:1.1.0"
implementation "androidx.legacy:legacy-support-v4:$rootProject.legacySupportVersion"
implementation "androidx.recyclerview:recyclerview:$rootProject.recyclerVersion"
implementation "androidx.constraintlayout:constraintlayout:$rootProject.constraintLayoutVersion"
implementation "androidx.gridlayout:gridlayout:$rootProject.gridLayoutVersion"
//Navigation
implementation "androidx.navigation:navigation-fragment-ktx:$rootProject.navigationVersion"
implementation "androidx.navigation:navigation-ui-ktx:$rootProject.navigationVersion"
}
// The sample build uses multiple directories to
......@@ -74,8 +84,12 @@ android {
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
sourceSets {
......@@ -94,9 +108,11 @@ android {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
manifestPlaceholders = [appTitle: "TouHou.FM" ]
}
debug {
applicationIdSuffix ".debug"
manifestPlaceholders = [appTitle: "[Debug] TouHou.FM" ]
}
}
......@@ -116,3 +132,4 @@ task printVersion() {
println("Version name: $gitVersionName")
println("Version code: $gitVersionCode")
}
<?xml version="1.0" encoding="utf-8"?>
<!--
* Copyright 2018 Daniel Sonck
*
* This file is part of fm.touhou.touhoufm.
*
* fm.touhou.touhoufm is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, version 2.
*
* fm.touhou.touhoufm is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with fm.touhou.touhoufm. If not, see <http://www.gnu.org/licenses/>.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="fm.touhou.touhoufm">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
xmlns:tools="http://schemas.android.com/tools"
package="fm.touhou.touhoufm">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:name=".TouHouFM">
android:name=".TouHouFM"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".ui.MainActivity"
android:launchMode="singleTop">
android:name=".ui.MainActivity"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="www.touhou.fm"
android:pathPattern="/app"
android:scheme="https" />
</intent-filter>
<tools:validation testUrl="https://www.touhou.fm/app" />
</activity>
<service
android:name=".service.MusicService"
android:enabled="true"
android:exported="true">
<receiver android:name=".widget.ControlsWidget">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
</service>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/controls_widget_info" />
</receiver>
<receiver android:name="androidx.media.session.MediaButtonReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<!--
MediaSession, prior to API 21, uses a broadcast receiver to communicate with a
media session. It does not have to be this broadcast receiver, but it must
......@@ -58,11 +56,18 @@
Additionally, this is used to resume the service from an inactive state upon
receiving a media button event (such as "play").
-->
<receiver android:name="android.support.v4.media.session.MediaButtonReceiver">
<service
android:name=".service.MusicService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON"/>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
</service>
</application>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
\ No newline at end of file
......@@ -40,7 +40,6 @@ import android.content.Context
import android.content.ServiceConnection
import android.os.IBinder
import android.os.RemoteException
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaControllerCompat
import android.support.v4.media.session.MediaSessionCompat
......@@ -68,19 +67,18 @@ class MediaBrowserAdapter(private val mContext: Context) {
val transportControls: MediaControllerCompat.TransportControls
get() {
return mMediaController.let { mediaController ->
if (mediaController == null) {
throw java.lang.IllegalStateException()
}
checkNotNull(mediaController)
mediaController.transportControls
}
}
/**
* Helper class for easily subscribing to changes in a MediaBrowserService connection.
*/
abstract class MediaBrowserChangeListener {
fun onConnected(mediaController: MediaControllerCompat?) {}
open fun onConnected(mediaController: MediaControllerCompat?) {}
open fun onMetadataChanged(mediaMetadata: MediaMetadataCompat?) {}
......@@ -129,7 +127,7 @@ class MediaBrowserAdapter(private val mContext: Context) {
private fun removeListener(listener: MediaBrowserChangeListener?) {
listener?.let {
if(mListeners.contains(it)) mListeners.remove(it)
if (mListeners.contains(it)) mListeners.remove(it)
}
}
......@@ -161,19 +159,19 @@ class MediaBrowserAdapter(private val mContext: Context) {
Log.d(TAG, "Session token is null")
return
}
val mediaController = MediaControllerCompat(mContext, token)
mMediaController = mediaController
mediaController.registerCallback(mMediaControllerCallback)
mMediaController = MediaControllerCompat(mContext, token).also { controller ->
performOnAllListeners(object : ListenerCommand {
override fun perform(listener: MediaBrowserChangeListener) {
listener.run {
onConnected(controller)
onMetadataChanged(controller.metadata)
onPlaybackStateChanged(controller.playbackState)
}
}
})
controller.registerCallback(MediaControllerCallback())
}
performOnAllListeners(object : ListenerCommand {
override fun perform(listener: MediaBrowserChangeListener) {
listener.onConnected(mediaController)
listener.onMetadataChanged(mediaController.metadata)
listener.onPlaybackStateChanged(mediaController.playbackState)
}
})
} catch (e: RemoteException) {
Log.d(TAG, "onConnected: Exception", e)
......
......@@ -20,39 +20,48 @@
package fm.touhou.touhoufm.radio
import android.media.AudioAttributes
import android.media.AudioAttributes.CONTENT_TYPE_MUSIC
import android.media.AudioAttributes.USAGE_MEDIA
import android.media.AudioFormat
import android.media.AudioManager
import android.media.AudioFormat.CHANNEL_OUT_STEREO
import android.media.AudioFormat.ENCODING_PCM_16BIT
import android.media.AudioManager.STREAM_MUSIC
import android.media.AudioTrack
import android.media.AudioTrack.MODE_STREAM
import android.os.Build
import android.util.Log
import android.media.AudioFormat.CHANNEL_OUT_STEREO
import android.media.AudioFormat.ENCODING_PCM_16BIT
import fm.touhou.touhoufm.radio.RtpReceiver.Companion.SAMPLE_RATE
class AndroidPlayer(private val mRingBuffer: RingBuffer, private val mMinBufSize: Int) : Thread() {
private var mStreamTrack: AudioTrack? = null
class AndroidPlayer(private val mRingBuffer: RingBuffer, private val mMinBufSize: Int) : Thread("AudioThread") {
private var mStreamTrack: AudioTrack =
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ->
AudioTrack.Builder().run {
setAudioAttributes(
AudioAttributes.Builder().run {
setUsage(USAGE_MEDIA)
setContentType(CONTENT_TYPE_MUSIC)
build()
}
)
setAudioFormat(
AudioFormat.Builder().run{
setEncoding(ENCODING_PCM_16BIT)
setSampleRate(SAMPLE_RATE)
setChannelMask(CHANNEL_OUT_STEREO)
build()
}
)
setBufferSizeInBytes(mMinBufSize)
build()
}
else ->
@Suppress("DEPRECATION")
AudioTrack(STREAM_MUSIC, SAMPLE_RATE, CHANNEL_OUT_STEREO, ENCODING_PCM_16BIT, mMinBufSize, MODE_STREAM)
}
init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mStreamTrack = AudioTrack.Builder()
.setAudioAttributes(AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build())
.setAudioFormat(AudioFormat.Builder()
.setEncoding(ENCODING_PCM_16BIT)
.setSampleRate(SAMPLE_RATE)
.setChannelMask(CHANNEL_OUT_STEREO).build())
.setBufferSizeInBytes(mMinBufSize).build()
} else {
@Suppress("DEPRECATION")
mStreamTrack = AudioTrack(AudioManager.STREAM_MUSIC, SAMPLE_RATE, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT, mMinBufSize, AudioTrack.MODE_STREAM)
}
Log.d(TAG, "AudioTrack min buffer size: ---- $mMinBufSize")
}
override fun run() {
......@@ -64,7 +73,7 @@ class AndroidPlayer(private val mRingBuffer: RingBuffer, private val mMinBufSize
}
private fun startup() {
mStreamTrack!!.play()
mStreamTrack.play()
Log.d(TAG, "Player started --- ")
}
......@@ -77,12 +86,12 @@ class AndroidPlayer(private val mRingBuffer: RingBuffer, private val mMinBufSize
while (!interrupted()) {
if (mRingBuffer.read(fullBuffer, fullBuffer.size) < 0) {
mStreamTrack!!.pause()
mStreamTrack!!.flush()
mStreamTrack.pause()
mStreamTrack.flush()
interrupt()
} else {
// Write out our full buffer
mStreamTrack!!.write(fullBuffer, 0, fullBuffer.size)
mStreamTrack.write(fullBuffer, 0, fullBuffer.size)
}
}
} catch (e: Exception) {
......@@ -93,23 +102,19 @@ class AndroidPlayer(private val mRingBuffer: RingBuffer, private val mMinBufSize
private fun shutDown() {
Log.d(TAG, "Player finished --- ")
val streamTrack = mStreamTrack
if (streamTrack != null) {
streamTrack.pause()
streamTrack.flush()
streamTrack.release()
mStreamTrack = null
}
mStreamTrack.pause()
mStreamTrack.flush()
mStreamTrack.release()
}
fun setVolume(gain: Float) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mStreamTrack?.setVolume(gain)
mStreamTrack.setVolume(gain)
} else {
@Suppress("DEPRECATION")
mStreamTrack?.setStereoVolume(gain, gain)
(mStreamTrack.setStereoVolume(gain, gain))
}
}
......
......@@ -19,24 +19,14 @@
package fm.touhou.touhoufm.radio
import android.provider.ContactsContract
import android.util.Log
import java.io.IOException
import java.net.DatagramPacket
import java.net.DatagramSocket
import java.net.InetAddress
import java.net.SocketException
import java.net.SocketTimeoutException
import java.net.UnknownHostException
import fm.touhou.touhoufm.service.players.RadioPlayer
import fm.touhou.touhoufm.utils.AudioPacket
import fm.touhou.touhoufm.utils.BasePacket
import fm.touhou.touhoufm.utils.MetaPacket
import fm.touhou.touhoufm.utils.OpusDecoder
import java.io.IOException
import java.net.*
class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener: MetaListener, private val progressListener: ProgressListener) : Thread() {
class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener: MetaListener, private val progressListener: ProgressListener) : Thread("RtpThread") {
private var mLastSeqNum = 0
......@@ -115,7 +105,7 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener:
throw e
} else {
try {
Thread.sleep(10000)
sleep(10000)
} catch (e1: InterruptedException) {
interrupt()
}
......@@ -145,10 +135,10 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener:
if (e.message == "Network is unreachable") {
try {
Thread.sleep(1000)
sleep(1000)
} catch (ignored: InterruptedException) {
Log.w(TAG, "Sleep interrupted")
Thread.currentThread().interrupt()
currentThread().interrupt()
}
} else {
......@@ -186,7 +176,7 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener:
val pcmFrames = ShortArray(BUF_SIZE)
// As long as we're not interrupted
while (!Thread.interrupted()) {
while (!interrupted()) {
if (System.currentTimeMillis() - mLastHello > 30000) {
try {
......@@ -268,7 +258,7 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener:
sendHello()
try {
Thread.sleep(1000)
sleep(1000)
} catch (ignored: InterruptedException) {
Log.w(TAG, "Sleep interrupted")
interrupt()
......@@ -295,7 +285,7 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener:
}
companion object {
private val TAG = RtpReceiver::class.java.getName()
private val TAG = RtpReceiver::class.java.simpleName
private const val SAMPLE_LEN = 40
const val SAMPLE_RATE = 48000
......
......@@ -35,32 +35,86 @@
package fm.touhou.touhoufm.service
import android.R
import android.app.Service
import android.content.Context
import android.content.Intent
import android.appwidget.AppWidgetManager
import android.content.*
import android.os.Binder
import android.os.IBinder
import android.support.v4.content.ContextCompat
import android.os.Parcel
import androidx.core.content.ContextCompat
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaButtonReceiver
import androidx.media.session.MediaButtonReceiver
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.support.v4.media.session.PlaybackStateCompat.*
import android.util.Log
import fm.touhou.touhoufm.service.notifications.MediaNotificationManager
import fm.touhou.touhoufm.service.players.MediaPlayerAdapter
import fm.touhou.touhoufm.utils.buildPkgString
import fm.touhou.touhoufm.widget.ControlsWidget
import fm.touhou.touhoufm.widget.ControlsWidget.Companion.ACTION_WIDGET_DISABLED
import fm.touhou.touhoufm.widget.ControlsWidget.Companion.ACTION_WIDGET_ENABLED
import fm.touhou.touhoufm.widget.ControlsWidget.Companion.ACTION_WIDGET_INIT
import fm.touhou.touhoufm.widget.ControlsWidget.Companion.EXTRA_MEDIA_METADATA
import fm.touhou.touhoufm.widget.ControlsWidget.Companion.EXTRA_PLAYBACK_STATE
class MusicService : Service() {
private var mSession: MediaSessionCompat? = null
private var mPlayback: PlayerAdapter? = null
private var mMediaNotificationManager: MediaNotificationManager? = null
private var widget = 0
private lateinit var mSession: MediaSessionCompat
private lateinit var mPlayback: PlayerAdapter
private lateinit var mMediaNotificationManager: MediaNotificationManager
private var mServiceInStartedState: Boolean = false
private val mLocalBinder = LocalBinder()
private var mShowNotification = false
val mediaSessionToken: MediaSessionCompat.Token?
get() = mSession?.sessionToken
get() = mSession.sessionToken
private val mReceiver = object : BroadcastReceiver() {
override fun onReceive(ctx: Context, intent: Intent) {
val action = intent.action ?: return
val state = intent.getIntExtra("state", 0)
when (action) {
ACTION_WIDGET_INIT -> updateWidget()
ACTION_WIDGET_ENABLED, ACTION_WIDGET_DISABLED -> updateHasWidget()
}
}
}
private fun updateWidget() {
if (widget != 0) {
updateWidgetState()
}
}
private fun updateWidgetState() {
val media = mPlayback.currentMedia ?: return
val playbackState = mPlayback.playbackState
val mediaIntent = Intent(ControlsWidget.ACTION_WIDGET_UPDATE)
mediaIntent.putExtra(EXTRA_MEDIA_METADATA, media)
mediaIntent.putExtra(EXTRA_PLAYBACK_STATE, playbackState)
sendWidgetBroadcast(mediaIntent)
}
private fun sendWidgetBroadcast(intent: Intent) {
intent.component = ComponentName(this, ControlsWidget::class.java)
sendBroadcast(intent)
}
private fun updateHasWidget() {
val manager = AppWidgetManager.getInstance(this) ?: return
widget = when {
manager.getAppWidgetIds(ComponentName(this, ControlsWidget::class.java)).isNotEmpty() -> 1
else -> 0
}
}
override fun onCreate() {
super.onCreate()
......@@ -73,16 +127,29 @@ class MusicService : Service() {
MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS or
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
)
isActive = true
}
mMediaNotificationManager = MediaNotificationManager(this)
mPlayback = MediaPlayerAdapter(this, MediaPlayerListener())
updateHasWidget()
Log.d(TAG, "onCreate: MusicService creating MediaSession, and MediaNotificationManager")
mSession.setPlaybackState(mPlayback.playbackState)
val filter = IntentFilter().apply {
priority = Integer.MAX_VALUE
addAction(ACTION_WIDGET_ENABLED)
addAction(ACTION_WIDGET_DISABLED)
addAction(ACTION_WIDGET_INIT)
}
registerReceiver(mReceiver, filter)
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
mSession?.let { session ->
mSession.let { session ->
moveServiceToStartedState(session.controller.playbackState)
MediaButtonReceiver.handleIntent(session, intent)
}
......@@ -95,9 +162,10 @@ class MusicService : Service() {
}
override fun onDestroy() {
mMediaNotificationManager?.onDestroy()
mPlayback?.stop()
mSession?.release()
unregisterReceiver(mReceiver)
mMediaNotificationManager.onDestroy()
mPlayback.stop()
mSession.release()
Log.d(TAG, "onDestroy: MediaPlayerAdapter stopped, and MediaSession released")
}
......@@ -109,30 +177,24 @@ class MusicService : Service() {
inner class MediaSessionCallback : MediaSessionCompat.Callback() {
override