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 host: String
abstract val name: String?
abstract val id: String?
}
class DiscoveredDisplayHost(
@ -31,6 +33,8 @@ class DiscoveredDisplayHost(
): DisplayHost()
{
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(
@ -39,4 +43,6 @@ class ManualDisplayHost(
): DisplayHost()
{
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
import androidx.lifecycle.LiveDataReactiveStreams
import io.reactivex.BackpressureStrategy
import io.reactivex.Observable
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 com.metallic.chiaki.lib.CreateError
import com.metallic.chiaki.lib.DiscoveryHost
import com.metallic.chiaki.lib.DiscoveryService
import com.metallic.chiaki.lib.DiscoveryServiceOptions
import io.reactivex.Observable
import io.reactivex.subjects.BehaviorSubject
import io.reactivex.subjects.Subject
import java.net.InetSocketAddress
class DiscoveryManager
@ -35,26 +39,61 @@ class DiscoveryManager
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)
return
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")
}
paused = false
updateService()
}
fun stop()
fun pause()
{
val service = discoveryService ?: return
service.dispose()
discoveryService = null
paused = true
updateService()
}
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(
options: DiscoveryServiceOptions,
val callback: ((hosts: List<DiscoveryHost>) -> Unit)? = null)
val callback: ((hosts: List<DiscoveryHost>) -> Unit)?)
{
private var nativePtr: Long

View file

@ -21,8 +21,10 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.metallic.chiaki.R
import com.metallic.chiaki.common.DiscoveredDisplayHost
import com.metallic.chiaki.common.DisplayHost
import com.metallic.chiaki.common.ext.inflate
import com.metallic.chiaki.lib.DiscoveryHost
import kotlinx.android.synthetic.main.item_display_host.view.*
class DisplayHostRecyclerViewAdapter: RecyclerView.Adapter<DisplayHostRecyclerViewAdapter.ViewHolder>()
@ -43,9 +45,24 @@ class DisplayHostRecyclerViewAdapter: RecyclerView.Adapter<DisplayHostRecyclerVi
override fun onBindViewHolder(holder: ViewHolder, position: Int)
{
val context = holder.itemView.context
val host = hosts[position]
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 lateinit var viewModel: MainViewModel
private var discoveryMenuItem: MenuItem? = null
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
@ -46,7 +50,6 @@ class MainActivity : AppCompatActivity()
title = ""
setSupportActionBar(toolbar)
addButton.setOnClickListener {
Intent(this, TestStartActivity::class.java).also {
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)) })
.get(MainViewModel::class.java)
@ -63,6 +66,10 @@ class MainActivity : AppCompatActivity()
hostsRecyclerView.adapter = recyclerViewAdapter
hostsRecyclerView.layoutManager = LinearLayoutManager(this)
viewModel.displayHosts.observe(this, Observer { recyclerViewAdapter.hosts = it })
viewModel.discoveryActive.observe(this, Observer { active ->
discoveryMenuItem?.let { updateDiscoveryMenuItem(it, active) }
})
}
override fun onDestroy()
@ -71,14 +78,29 @@ class MainActivity : AppCompatActivity()
disposable.dispose()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean
override fun onCreateOptionsMenu(menu: Menu): Boolean
{
menuInflater.inflate(R.menu.main, menu)
val discoveryItem = menu.findItem(R.id.action_discover)
discoveryMenuItem = discoveryItem
updateDiscoveryMenuItem(discoveryItem, viewModel.discoveryActive.value ?: false)
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)
{
R.id.action_discover ->
{
viewModel.discoveryManager.active = !(viewModel.discoveryActive.value ?: false)
true
}
R.id.action_settings ->
{
Intent(this, SettingsActivity::class.java).also {

View file

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

View file

@ -1,4 +1,4 @@
<vector android:height="32dp" android:viewportHeight="24"
android:viewportWidth="24" android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android">
<vector android:height="24dp" android:viewportHeight="24"
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"/>
</vector>

View file

@ -1,4 +1,4 @@
<vector android:height="32dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android">
<vector android:height="24dp" android:viewportHeight="24.0"
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"/>
</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:elevation="8dp">
<ImageView
android:layout_width="72dp"
android:layout_height="72dp"
android:src="@drawable/ic_triangle"
android:layout_gravity="left|top" />
<FrameLayout
android:id="@+id/discoveredIndicatorLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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
android:layout_width="match_parent"
android:layout_height="match_parent"
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
android:id="@+id/nameTextView"
android:layout_width="match_parent"
@ -67,6 +74,7 @@
android:textColor="?attr/colorOnSurface"/>
<ImageView
android:id="@+id/stateIndicatorImageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:src="@drawable/ic_console"

View file

@ -10,4 +10,6 @@
<string name="action_login_pin_connect">Connect</string>
<string name="action_settings">Settings</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>