Show Discovered Hosts in MainActivity on Android

This commit is contained in:
Florian Märkl 2019-10-05 16:25:22 +02:00
commit 53fe9e50e4
No known key found for this signature in database
GPG key ID: 125BC8A5A6A1E857
12 changed files with 149 additions and 213 deletions

View file

@ -23,6 +23,8 @@ sealed class DisplayHost
{ {
abstract val registeredHost: RegisteredHost? abstract val registeredHost: RegisteredHost?
abstract val host: String abstract val host: String
abstract val name: String?
abstract val id: String?
} }
class DiscoveredDisplayHost( class DiscoveredDisplayHost(
@ -31,6 +33,8 @@ class DiscoveredDisplayHost(
): DisplayHost() ): DisplayHost()
{ {
override val host get() = discoveredHost.hostAddr ?: "" override val host get() = discoveredHost.hostAddr ?: ""
override val name get() = discoveredHost.hostName ?: registeredHost?.ps4Nickname
override val id get() = discoveredHost.hostId ?: registeredHost?.ps4Mac?.toString()
} }
class ManualDisplayHost( class ManualDisplayHost(
@ -39,4 +43,6 @@ class ManualDisplayHost(
): DisplayHost() ): DisplayHost()
{ {
override val host get() = manualHost.host override val host get() = manualHost.host
override val name get() = registeredHost?.ps4Nickname
override val id get() = registeredHost?.ps4Mac?.toString()
} }

View file

@ -18,6 +18,9 @@
package com.metallic.chiaki.common.ext package com.metallic.chiaki.common.ext
import androidx.lifecycle.LiveDataReactiveStreams import androidx.lifecycle.LiveDataReactiveStreams
import io.reactivex.BackpressureStrategy
import io.reactivex.Observable
import org.reactivestreams.Publisher import org.reactivestreams.Publisher
fun <T> Publisher<T>.toLiveData() = LiveDataReactiveStreams.fromPublisher(this) fun <T> Publisher<T>.toLiveData() = LiveDataReactiveStreams.fromPublisher(this)
fun <T> Observable<T>.toLiveData() = this.toFlowable(BackpressureStrategy.LATEST).toLiveData()

View file

@ -19,8 +19,12 @@ package com.metallic.chiaki.discovery
import android.util.Log import android.util.Log
import com.metallic.chiaki.lib.CreateError import com.metallic.chiaki.lib.CreateError
import com.metallic.chiaki.lib.DiscoveryHost
import com.metallic.chiaki.lib.DiscoveryService import com.metallic.chiaki.lib.DiscoveryService
import com.metallic.chiaki.lib.DiscoveryServiceOptions import com.metallic.chiaki.lib.DiscoveryServiceOptions
import io.reactivex.Observable
import io.reactivex.subjects.BehaviorSubject
import io.reactivex.subjects.Subject
import java.net.InetSocketAddress import java.net.InetSocketAddress
class DiscoveryManager class DiscoveryManager
@ -35,26 +39,61 @@ class DiscoveryManager
private var discoveryService: DiscoveryService? = null private var discoveryService: DiscoveryService? = null
fun start() private val discoveryActiveSubject: Subject<Boolean> = BehaviorSubject.create<Boolean>().also { it.onNext(false) }
val discoveryActive: Observable<Boolean> get() = discoveryActiveSubject
var active = false
set(value)
{
field = value
discoveryActiveSubject.onNext(value)
updateService()
}
private var paused = false
private var discoveredHostsSubject: Subject<List<DiscoveryHost>> = BehaviorSubject.create<List<DiscoveryHost>>().also {
it.onNext(listOf())
}.toSerialized()
val discoveredHosts: Observable<List<DiscoveryHost>> get() = discoveredHostsSubject
fun resume()
{ {
if(discoveryService != null) paused = false
return updateService()
try
{
discoveryService = DiscoveryService(DiscoveryServiceOptions(
HOSTS_MAX, DROP_PINGS, PING_MS, InetSocketAddress("255.255.255.255", PORT)
))
}
catch(e: CreateError)
{
Log.e("DiscoveryManager", "Failed to start Discovery Service: $e")
}
} }
fun stop() fun pause()
{ {
val service = discoveryService ?: return paused = true
service.dispose() updateService()
discoveryService = null }
fun dispose()
{
active = false
}
private fun updateService()
{
if(active && !paused && discoveryService == null)
{
try
{
discoveryService = DiscoveryService(DiscoveryServiceOptions(
HOSTS_MAX, DROP_PINGS, PING_MS, InetSocketAddress("255.255.255.255", PORT)
), discoveredHostsSubject::onNext)
}
catch(e: CreateError)
{
Log.e("DiscoveryManager", "Failed to start Discovery Service: $e")
}
}
else if(discoveryService != null)
{
val service = discoveryService ?: return
service.dispose()
discoveryService = null
discoveredHostsSubject.onNext(listOf())
discoveryActiveSubject.onNext(false)
}
} }
} }

View file

@ -212,7 +212,7 @@ data class DiscoveryServiceOptions(
class DiscoveryService( class DiscoveryService(
options: DiscoveryServiceOptions, options: DiscoveryServiceOptions,
val callback: ((hosts: List<DiscoveryHost>) -> Unit)? = null) val callback: ((hosts: List<DiscoveryHost>) -> Unit)?)
{ {
private var nativePtr: Long private var nativePtr: Long

View file

@ -21,8 +21,10 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.metallic.chiaki.R import com.metallic.chiaki.R
import com.metallic.chiaki.common.DiscoveredDisplayHost
import com.metallic.chiaki.common.DisplayHost import com.metallic.chiaki.common.DisplayHost
import com.metallic.chiaki.common.ext.inflate import com.metallic.chiaki.common.ext.inflate
import com.metallic.chiaki.lib.DiscoveryHost
import kotlinx.android.synthetic.main.item_display_host.view.* import kotlinx.android.synthetic.main.item_display_host.view.*
class DisplayHostRecyclerViewAdapter: RecyclerView.Adapter<DisplayHostRecyclerViewAdapter.ViewHolder>() class DisplayHostRecyclerViewAdapter: RecyclerView.Adapter<DisplayHostRecyclerViewAdapter.ViewHolder>()
@ -43,9 +45,24 @@ class DisplayHostRecyclerViewAdapter: RecyclerView.Adapter<DisplayHostRecyclerVi
override fun onBindViewHolder(holder: ViewHolder, position: Int) override fun onBindViewHolder(holder: ViewHolder, position: Int)
{ {
val context = holder.itemView.context
val host = hosts[position] val host = hosts[position]
holder.itemView.also { holder.itemView.also {
it.hostTextView.text = host.host it.nameTextView.text = host.name
it.hostTextView.text = context.getString(R.string.display_host_host, host.host)
val id = host.id
it.idTextView.text = if(id != null) context.getString(R.string.display_host_id, id) else ""
it.discoveredIndicatorLayout.visibility = if(host is DiscoveredDisplayHost) View.VISIBLE else View.GONE
it.stateIndicatorImageView.setImageResource(
if(host is DiscoveredDisplayHost)
when(host.discoveredHost.state)
{
DiscoveryHost.State.STANDBY -> R.drawable.ic_console_standby
DiscoveryHost.State.READY -> R.drawable.ic_console_ready
else -> R.drawable.ic_console
}
else
R.drawable.ic_console)
} }
} }
} }

View file

@ -38,6 +38,10 @@ class MainActivity : AppCompatActivity()
{ {
private val disposable = CompositeDisposable() private val disposable = CompositeDisposable()
private lateinit var viewModel: MainViewModel
private var discoveryMenuItem: MenuItem? = null
override fun onCreate(savedInstanceState: Bundle?) override fun onCreate(savedInstanceState: Bundle?)
{ {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -46,7 +50,6 @@ class MainActivity : AppCompatActivity()
title = "" title = ""
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
addButton.setOnClickListener { addButton.setOnClickListener {
Intent(this, TestStartActivity::class.java).also { Intent(this, TestStartActivity::class.java).also {
it.putExtra(TestStartActivity.EXTRA_REVEAL_X, addButton.x + addButton.width * 0.5f) it.putExtra(TestStartActivity.EXTRA_REVEAL_X, addButton.x + addButton.width * 0.5f)
@ -55,7 +58,7 @@ class MainActivity : AppCompatActivity()
} }
} }
val viewModel = ViewModelProviders viewModel = ViewModelProviders
.of(this, viewModelFactory { MainViewModel(getDatabase(this)) }) .of(this, viewModelFactory { MainViewModel(getDatabase(this)) })
.get(MainViewModel::class.java) .get(MainViewModel::class.java)
@ -63,6 +66,10 @@ class MainActivity : AppCompatActivity()
hostsRecyclerView.adapter = recyclerViewAdapter hostsRecyclerView.adapter = recyclerViewAdapter
hostsRecyclerView.layoutManager = LinearLayoutManager(this) hostsRecyclerView.layoutManager = LinearLayoutManager(this)
viewModel.displayHosts.observe(this, Observer { recyclerViewAdapter.hosts = it }) viewModel.displayHosts.observe(this, Observer { recyclerViewAdapter.hosts = it })
viewModel.discoveryActive.observe(this, Observer { active ->
discoveryMenuItem?.let { updateDiscoveryMenuItem(it, active) }
})
} }
override fun onDestroy() override fun onDestroy()
@ -71,14 +78,29 @@ class MainActivity : AppCompatActivity()
disposable.dispose() disposable.dispose()
} }
override fun onCreateOptionsMenu(menu: Menu?): Boolean override fun onCreateOptionsMenu(menu: Menu): Boolean
{ {
menuInflater.inflate(R.menu.main, menu) menuInflater.inflate(R.menu.main, menu)
val discoveryItem = menu.findItem(R.id.action_discover)
discoveryMenuItem = discoveryItem
updateDiscoveryMenuItem(discoveryItem, viewModel.discoveryActive.value ?: false)
return true return true
} }
private fun updateDiscoveryMenuItem(item: MenuItem, active: Boolean)
{
item.isChecked = active
item.setIcon(if(active) R.drawable.ic_discover_on else R.drawable.ic_discover_off)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when(item.itemId) override fun onOptionsItemSelected(item: MenuItem): Boolean = when(item.itemId)
{ {
R.id.action_discover ->
{
viewModel.discoveryManager.active = !(viewModel.discoveryActive.value ?: false)
true
}
R.id.action_settings -> R.id.action_settings ->
{ {
Intent(this, SettingsActivity::class.java).also { Intent(this, SettingsActivity::class.java).also {

View file

@ -19,27 +19,36 @@ package com.metallic.chiaki.main
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.metallic.chiaki.common.AppDatabase import com.metallic.chiaki.common.AppDatabase
import com.metallic.chiaki.common.DiscoveredDisplayHost
import com.metallic.chiaki.common.ManualDisplayHost import com.metallic.chiaki.common.ManualDisplayHost
import com.metallic.chiaki.common.ext.toLiveData import com.metallic.chiaki.common.ext.toLiveData
import com.metallic.chiaki.discovery.DiscoveryManager import com.metallic.chiaki.discovery.DiscoveryManager
import io.reactivex.rxkotlin.Observables
class MainViewModel(val database: AppDatabase): ViewModel() class MainViewModel(val database: AppDatabase): ViewModel()
{ {
val discoveryManager = DiscoveryManager().also { it.active = true /* TODO: from shared preferences */ }
val displayHosts by lazy { val displayHosts by lazy {
database.manualHostDao().getAll() Observables.combineLatest(database.manualHostDao().getAll().toObservable(), discoveryManager.discoveredHosts)
.map { { manualHosts, discoveredHosts ->
it.map { manualHost -> discoveredHosts.map {
ManualDisplayHost(null, manualHost) DiscoveredDisplayHost(null /* TODO */, it)
} +
manualHosts.map {
ManualDisplayHost(null /* TODO */, it)
} }
} }
.toLiveData() .toLiveData()
} }
val discoveryManager = DiscoveryManager().also { it.start() } val discoveryActive by lazy {
discoveryManager.discoveryActive.toLiveData()
}
override fun onCleared() override fun onCleared()
{ {
super.onCleared() super.onCleared()
discoveryManager.stop() discoveryManager.dispose()
} }
} }

View file

@ -1,4 +1,4 @@
<vector android:height="32dp" android:viewportHeight="24" <vector android:height="24dp" android:viewportHeight="24"
android:viewportWidth="24" android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?attr/colorOnSurface" android:pathData="M22.99,9C19.15,5.16 13.8,3.76 8.84,4.78l2.52,2.52c3.47,-0.17 6.99,1.05 9.63,3.7l2,-2zM18.99,13c-1.29,-1.29 -2.84,-2.13 -4.49,-2.56l3.53,3.53 0.96,-0.97zM2,3.05L5.07,6.1C3.6,6.82 2.22,7.78 1,9l1.99,2c1.24,-1.24 2.67,-2.16 4.2,-2.77l2.24,2.24C7.81,10.89 6.27,11.73 5,13v0.01L6.99,15c1.36,-1.36 3.14,-2.04 4.92,-2.06L18.98,20l1.27,-1.26L3.29,1.79 2,3.05zM9,17l3,3 3,-3c-1.65,-1.66 -4.34,-1.66 -6,0z"/> <path android:fillColor="?attr/colorOnSurface" android:pathData="M22.99,9C19.15,5.16 13.8,3.76 8.84,4.78l2.52,2.52c3.47,-0.17 6.99,1.05 9.63,3.7l2,-2zM18.99,13c-1.29,-1.29 -2.84,-2.13 -4.49,-2.56l3.53,3.53 0.96,-0.97zM2,3.05L5.07,6.1C3.6,6.82 2.22,7.78 1,9l1.99,2c1.24,-1.24 2.67,-2.16 4.2,-2.77l2.24,2.24C7.81,10.89 6.27,11.73 5,13v0.01L6.99,15c1.36,-1.36 3.14,-2.04 4.92,-2.06L18.98,20l1.27,-1.26L3.29,1.79 2,3.05zM9,17l3,3 3,-3c-1.65,-1.66 -4.34,-1.66 -6,0z"/>
</vector> </vector>

View file

@ -1,4 +1,4 @@
<vector android:height="32dp" android:viewportHeight="24.0" <vector android:height="24dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?attr/colorOnSurface" android:pathData="M1,9l2,2c4.97,-4.97 13.03,-4.97 18,0l2,-2C16.93,2.93 7.08,2.93 1,9zM9,17l3,3 3,-3c-1.65,-1.66 -4.34,-1.66 -6,0zM5,13l2,2c2.76,-2.76 7.24,-2.76 10,0l2,-2C15.14,9.14 8.87,9.14 5,13z"/> <path android:fillColor="?attr/colorOnSurface" android:pathData="M1,9l2,2c4.97,-4.97 13.03,-4.97 18,0l2,-2C16.93,2.93 7.08,2.93 1,9zM9,17l3,3 3,-3c-1.65,-1.66 -4.34,-1.66 -6,0zM5,13l2,2c2.76,-2.76 7.24,-2.76 10,0l2,-2C15.14,9.14 8.87,9.14 5,13z"/>
</vector> </vector>

View file

@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#008577"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View file

@ -17,22 +17,29 @@
android:layout_marginBottom="0dp" android:layout_marginBottom="0dp"
android:elevation="8dp"> android:elevation="8dp">
<ImageView <FrameLayout
android:layout_width="72dp" android:id="@+id/discoveredIndicatorLayout"
android:layout_height="72dp" android:layout_width="wrap_content"
android:src="@drawable/ic_triangle" android:layout_height="wrap_content"
android:layout_gravity="left|top" /> android:layout_gravity="left|top" >
<ImageView
android:layout_width="72dp"
android:layout_height="72dp"
android:src="@drawable/ic_triangle"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_discovered_on_secondary"
android:layout_gravity="left|top"
android:layout_margin="8dp" />
</FrameLayout>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:padding="8dp"> android:padding="8dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_discovered_on_secondary" />
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/nameTextView" android:id="@+id/nameTextView"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -67,6 +74,7 @@
android:textColor="?attr/colorOnSurface"/> android:textColor="?attr/colorOnSurface"/>
<ImageView <ImageView
android:id="@+id/stateIndicatorImageView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:src="@drawable/ic_console" android:src="@drawable/ic_console"

View file

@ -10,4 +10,6 @@
<string name="action_login_pin_connect">Connect</string> <string name="action_login_pin_connect">Connect</string>
<string name="action_settings">Settings</string> <string name="action_settings">Settings</string>
<string name="action_discover">Discover Consoles Automatically</string> <string name="action_discover">Discover Consoles Automatically</string>
<string name="display_host_host">Address: %s</string>
<string name="display_host_id">ID: %s</string>
</resources> </resources>