Add Manual Host Editing on Android

This commit is contained in:
Florian Märkl 2019-10-27 17:41:54 +01:00
commit ffa381334a
No known key found for this signature in database
GPG key ID: 125BC8A5A6A1E857
18 changed files with 230 additions and 35 deletions

View file

@ -44,7 +44,7 @@
android:configChanges="keyboard|keyboardHidden|orientation|screenSize" />
<activity
android:name=".manualconsole.AddManualConsoleActivity"
android:name=".manualconsole.EditManualConsoleActivity"
android:theme="@style/MageTheme"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize" />
</application>

View file

@ -21,6 +21,7 @@ import androidx.room.*
import androidx.room.ForeignKey.SET_NULL
import io.reactivex.Completable
import io.reactivex.Flowable
import io.reactivex.Single
@Entity(tableName = "manual_host",
foreignKeys = [
@ -37,12 +38,37 @@ data class ManualHost(
@ColumnInfo(name = "registered_host") val registeredHost: Long?
)
data class ManualHostAndRegisteredHost(
@Embedded(prefix = "manual_host_") val manualHost: ManualHost,
@Embedded val registeredHost: RegisteredHost?
)
@Dao
interface ManualHostDao
{
@Query("SELECT * FROM manual_host WHERE id = :id")
fun getById(id: Long): Single<ManualHost>
@Query("""SELECT
manual_host.id as manual_host_id,
manual_host.host as manual_host_host,
manual_host.registered_host as manual_host_registered_host,
registered_host.*
FROM manual_host LEFT OUTER JOIN registered_host ON manual_host.registered_host = registered_host.id WHERE manual_host.id = :id""")
fun getByIdWithRegisteredHost(id: Long): Single<ManualHostAndRegisteredHost>
@Query("SELECT * FROM manual_host")
fun getAll(): Flowable<List<ManualHost>>
@Query("UPDATE manual_host SET registered_host = :registeredHostId WHERE id = :manualHostId")
fun assignRegisteredHost(manualHostId: Long, registeredHostId: Long?): Completable
@Insert
fun insert(host: ManualHost): Completable
@Delete
fun delete(host: ManualHost): Completable
@Update
fun update(host: ManualHost): Completable
}

View file

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

View file

@ -21,11 +21,15 @@ import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.view.animation.AnimationUtils
import android.widget.PopupMenu
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
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.ManualDisplayHost
import com.metallic.chiaki.common.ext.inflate
import com.metallic.chiaki.lib.DiscoveryHost
import kotlinx.android.synthetic.main.item_display_host.view.*
@ -38,7 +42,12 @@ class DisplayHostDiffCallback(val old: List<DisplayHost>, val new: List<DisplayH
override fun getNewListSize() = new.size
}
class DisplayHostRecyclerViewAdapter(val clickCallback: (DisplayHost) -> Unit): RecyclerView.Adapter<DisplayHostRecyclerViewAdapter.ViewHolder>()
class DisplayHostRecyclerViewAdapter(
val clickCallback: (DisplayHost) -> Unit,
val wakeupCallback: (DisplayHost) -> Unit,
val editCallback: (DisplayHost) -> Unit,
val deleteCallback: (DisplayHost) -> Unit
): RecyclerView.Adapter<DisplayHostRecyclerViewAdapter.ViewHolder>()
{
var hosts: List<DisplayHost> = listOf()
set(value)
@ -91,6 +100,36 @@ class DisplayHostRecyclerViewAdapter(val clickCallback: (DisplayHost) -> Unit):
else
R.drawable.ic_console)
it.setOnClickListener { clickCallback(host) }
val canWakeup = host.registeredHost != null
val canEditDelete = host is ManualDisplayHost
if(canWakeup || canEditDelete)
{
it.menuButton.isVisible = true
it.menuButton.setOnClickListener { _ ->
val menu = PopupMenu(context, it.menuButton)
menu.menuInflater.inflate(R.menu.display_host, menu.menu)
menu.menu.findItem(R.id.action_wakeup).isVisible = canWakeup
menu.menu.findItem(R.id.action_edit).isVisible = canEditDelete
menu.menu.findItem(R.id.action_delete).isVisible = canEditDelete
menu.setOnMenuItemClickListener { menuItem ->
when(menuItem.itemId)
{
R.id.action_wakeup -> wakeupCallback(host)
R.id.action_edit -> editCallback(host)
R.id.action_delete -> deleteCallback(host)
else -> return@setOnMenuItemClickListener false
}
true
}
menu.show()
}
}
else
{
it.menuButton.isGone = true
it.menuButton.setOnClickListener(null)
}
}
}
}

View file

@ -28,15 +28,12 @@ import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.metallic.chiaki.R
import com.metallic.chiaki.common.DiscoveredDisplayHost
import com.metallic.chiaki.common.DisplayHost
import com.metallic.chiaki.common.Preferences
import com.metallic.chiaki.common.*
import com.metallic.chiaki.common.ext.putRevealExtra
import com.metallic.chiaki.common.ext.viewModelFactory
import com.metallic.chiaki.common.getDatabase
import com.metallic.chiaki.lib.ConnectInfo
import com.metallic.chiaki.lib.DiscoveryHost
import com.metallic.chiaki.manualconsole.AddManualConsoleActivity
import com.metallic.chiaki.manualconsole.EditManualConsoleActivity
import com.metallic.chiaki.regist.RegistActivity
import com.metallic.chiaki.settings.SettingsActivity
import com.metallic.chiaki.stream.StreamActivity
@ -73,7 +70,7 @@ class MainActivity : AppCompatActivity()
.of(this, viewModelFactory { MainViewModel(getDatabase(this), Preferences(this)) })
.get(MainViewModel::class.java)
val recyclerViewAdapter = DisplayHostRecyclerViewAdapter(this::hostTriggered)
val recyclerViewAdapter = DisplayHostRecyclerViewAdapter(this::hostTriggered, this::wakeupHost, this::editHost, this::deleteHost)
hostsRecyclerView.adapter = recyclerViewAdapter
hostsRecyclerView.layoutManager = LinearLayoutManager(this)
viewModel.displayHosts.observe(this, Observer {
@ -153,7 +150,7 @@ class MainActivity : AppCompatActivity()
private fun addManualConsole()
{
Intent(this, AddManualConsoleActivity::class.java).also {
Intent(this, EditManualConsoleActivity::class.java).also {
it.putRevealExtra(addManualButton, rootLayout)
startActivity(it, ActivityOptions.makeSceneTransitionAnimation(this).toBundle())
}
@ -185,7 +182,7 @@ class MainActivity : AppCompatActivity()
MaterialAlertDialogBuilder(this)
.setMessage(R.string.alert_message_standby_wakeup)
.setPositiveButton(R.string.action_wakeup) { _, _ ->
viewModel.discoveryManager.sendWakeup(host.host, registeredHost.rpRegistKey)
wakeupHost(host)
}
.setNeutralButton(R.string.action_connect_immediately) { _, _ ->
connect()
@ -202,8 +199,40 @@ class MainActivity : AppCompatActivity()
Intent(this, RegistActivity::class.java).let {
it.putExtra(RegistActivity.EXTRA_HOST, host.host)
it.putExtra(RegistActivity.EXTRA_BROADCAST, false)
if(host is ManualDisplayHost)
it.putExtra(RegistActivity.EXTRA_ASSIGN_MANUAL_HOST_ID, host.manualHost.id)
startActivity(it)
}
}
}
private fun wakeupHost(host: DisplayHost)
{
val registeredHost = host.registeredHost ?: return
viewModel.discoveryManager.sendWakeup(host.host, registeredHost.rpRegistKey)
}
private fun editHost(host: DisplayHost)
{
if(host !is ManualDisplayHost)
return
Intent(this, EditManualConsoleActivity::class.java).also {
it.putExtra(EditManualConsoleActivity.EXTRA_MANUAL_HOST_ID, host.manualHost.id)
startActivity(it)
}
}
private fun deleteHost(host: DisplayHost)
{
if(host !is ManualDisplayHost)
return
MaterialAlertDialogBuilder(this)
.setMessage(getString(R.string.alert_message_delete_manual_host, host.manualHost.host))
.setPositiveButton(R.string.action_delete) { _, _ ->
viewModel.deleteManualHost(host.manualHost)
}
.setNegativeButton(R.string.action_keep) { _, _ -> }
.create()
.show()
}
}

View file

@ -18,10 +18,7 @@
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.Preferences
import com.metallic.chiaki.common.*
import com.metallic.chiaki.common.ext.toLiveData
import com.metallic.chiaki.discovery.DiscoveryManager
import com.metallic.chiaki.discovery.ps4Mac
@ -29,6 +26,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.Observables
import io.reactivex.rxkotlin.addTo
import io.reactivex.schedulers.Schedulers
class MainViewModel(val database: AppDatabase, val preferences: Preferences): ViewModel()
{
@ -64,6 +62,16 @@ class MainViewModel(val database: AppDatabase, val preferences: Preferences): Vi
discoveryManager.discoveryActive.toLiveData()
}
fun deleteManualHost(manualHost: ManualHost)
{
database.manualHostDao()
.delete(manualHost)
.onErrorComplete()
.subscribeOn(Schedulers.io())
.subscribe()
.addTo(disposable)
}
override fun onCleared()
{
super.onCleared()

View file

@ -22,13 +22,11 @@ import android.os.Bundle
import android.view.View
import android.view.Window
import android.widget.AdapterView
import android.widget.AdapterView.OnItemSelectedListener
import android.widget.ArrayAdapter
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import com.metallic.chiaki.R
import com.metallic.chiaki.common.ManualHost
import com.metallic.chiaki.common.RegisteredHost
import com.metallic.chiaki.common.ext.RevealActivity
import com.metallic.chiaki.common.ext.viewModelFactory
@ -36,27 +34,42 @@ import com.metallic.chiaki.common.getDatabase
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.addTo
import kotlinx.android.synthetic.main.activity_add_manual.*
import kotlinx.android.synthetic.main.activity_edit_manual.*
class AddManualConsoleActivity: AppCompatActivity(), RevealActivity
class EditManualConsoleActivity: AppCompatActivity(), RevealActivity
{
companion object
{
const val EXTRA_MANUAL_HOST_ID = "manual_host_id"
}
override val revealIntent: Intent get() = intent
override val revealRootLayout: View get() = rootLayout
override val revealWindow: Window get() = window
private lateinit var viewModel: AddManualConsoleViewModel
private lateinit var viewModel: EditManualConsoleViewModel
private val disposable = CompositeDisposable()
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_manual)
setContentView(R.layout.activity_edit_manual)
handleReveal()
viewModel = ViewModelProviders
.of(this, viewModelFactory { AddManualConsoleViewModel(getDatabase(this)) })
.get(AddManualConsoleViewModel::class.java)
.of(this, viewModelFactory {
EditManualConsoleViewModel(getDatabase(this),
if(intent.hasExtra(EXTRA_MANUAL_HOST_ID))
intent.getLongExtra(EXTRA_MANUAL_HOST_ID, 0)
else
null)
})
.get(EditManualConsoleViewModel::class.java)
viewModel.existingHost?.observe(this, Observer {
hostEditText.setText(it.host)
})
viewModel.selectedRegisteredHost.observe(this, Observer {
registeredHostTextView.setText(titleForRegisteredHost(it))

View file

@ -17,6 +17,8 @@
package com.metallic.chiaki.manualconsole
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.metallic.chiaki.common.AppDatabase
@ -26,7 +28,7 @@ import com.metallic.chiaki.common.ext.toLiveData
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
class AddManualConsoleViewModel(val database: AppDatabase): ViewModel()
class EditManualConsoleViewModel(val database: AppDatabase, manualHostId: Long?): ViewModel()
{
val registeredHosts by lazy {
database.registeredHostDao().getAll().observeOn(AndroidSchedulers.mainThread())
@ -39,10 +41,35 @@ class AddManualConsoleViewModel(val database: AppDatabase): ViewModel()
.toLiveData()
}
val existingHost: LiveData<ManualHost>? =
if(manualHostId != null)
database.manualHostDao()
.getByIdWithRegisteredHost(manualHostId)
.toFlowable()
.doOnError {
Log.e("EditManualConsole", "Failed to fetch existing manual host", it)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { hosts ->
selectedRegisteredHost.value = hosts.registeredHost
}
.map { hosts -> hosts.manualHost }
.toLiveData()
else
null
var selectedRegisteredHost = MutableLiveData<RegisteredHost?>(null)
fun saveHost(host: String) =
database.manualHostDao()
.insert(ManualHost(host = host, registeredHost = selectedRegisteredHost.value?.id))
.let {
val registeredHost = selectedRegisteredHost.value?.id
val existingHost = existingHost?.value
if(existingHost != null)
it.update(ManualHost(id = existingHost.id, host = host, registeredHost = registeredHost))
else
it.insert(ManualHost(host = host, registeredHost = registeredHost))
}
.subscribeOn(Schedulers.io())
}

View file

@ -37,6 +37,7 @@ class RegistActivity: AppCompatActivity(), RevealActivity
{
const val EXTRA_HOST = "regist_host"
const val EXTRA_BROADCAST = "regist_broadcast"
const val EXTRA_ASSIGN_MANUAL_HOST_ID = "assign_manual_host_id"
private const val PIN_LENGTH = 8
@ -130,6 +131,8 @@ class RegistActivity: AppCompatActivity(), RevealActivity
Intent(this, RegistExecuteActivity::class.java).also {
it.putExtra(RegistExecuteActivity.EXTRA_REGIST_INFO, registInfo)
if(intent.hasExtra(EXTRA_ASSIGN_MANUAL_HOST_ID))
it.putExtra(RegistExecuteActivity.EXTRA_ASSIGN_MANUAL_HOST_ID, intent.getLongExtra(EXTRA_ASSIGN_MANUAL_HOST_ID, 0L))
startActivityForResult(it, REQUEST_REGIST)
}
}

View file

@ -40,6 +40,7 @@ class RegistExecuteActivity: AppCompatActivity()
companion object
{
const val EXTRA_REGIST_INFO = "regist_info"
const val EXTRA_ASSIGN_MANUAL_HOST_ID = "assign_manual_host_id"
const val RESULT_FAILED = Activity.RESULT_FIRST_USER
}
@ -105,7 +106,11 @@ class RegistExecuteActivity: AppCompatActivity()
finish()
return
}
viewModel.start(registInfo)
viewModel.start(registInfo,
if(intent.hasExtra(EXTRA_ASSIGN_MANUAL_HOST_ID))
intent.getLongExtra(EXTRA_ASSIGN_MANUAL_HOST_ID, 0)
else
null)
}
override fun onStop()

View file

@ -56,13 +56,16 @@ class RegistExecuteViewModel(val database: AppDatabase): ViewModel()
var host: RegistHost? = null
private set
fun start(info: RegistInfo)
private var assignManualHostId: Long? = null
fun start(info: RegistInfo, assignManualHostId: Long?)
{
if(regist != null)
return
try
{
regist = Regist(info, log.log, this::registEvent)
this.assignManualHostId = assignManualHostId
_state.value = State.RUNNING
}
catch(error: CreateError)
@ -106,13 +109,23 @@ class RegistExecuteViewModel(val database: AppDatabase): ViewModel()
fun saveHost()
{
val host = host ?: return
val assignManualHostId = assignManualHostId
val dao = database.registeredHostDao()
val manualHostDao = database.manualHostDao()
val registeredHost = RegisteredHost(host)
dao.deleteByMac(registeredHost.ps4Mac)
.andThen(dao.insert(registeredHost))
.let {
if(assignManualHostId != null)
it.flatMapCompletable { registeredHostId ->
manualHostDao.assignRegisteredHost(assignManualHostId, registeredHostId)
}
else
it.ignoreElement()
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { _ -> /* No, IntelliJ, this "_ ->" IS necessary. */
.subscribe {
Log.i("RegistExecute", "Registered Host saved in db")
_state.value = State.SUCCESSFUL
}

View file

@ -62,10 +62,10 @@ class SettingsRegisteredHostsFragment: AppCompatDialogFragment(), TitleFragment
val host = viewModel.registeredHosts.value?.getOrNull(pos) ?: return
MaterialAlertDialogBuilder(viewHolder.itemView.context)
.setMessage(getString(R.string.alert_message_delete_registered_host, host.ps4Nickname, host.ps4Mac.toString()))
.setPositiveButton(R.string.alert_action_delete_registered_host) { _, _ ->
.setPositiveButton(R.string.action_delete) { _, _ ->
viewModel.deleteHost(host)
}
.setNegativeButton(R.string.alert_action_keep_registered_host) { _, _ ->
.setNegativeButton(R.string.action_keep) { _, _ ->
adapter.notifyItemChanged(pos) // to reset the swipe
}
.create()

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="?attr/colorOnSurface"
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
</vector>

View file

@ -30,7 +30,7 @@
android:id="@+id/titleTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/title_add_manual"
android:text="@string/title_edit_manual"
android:textSize="32sp"
android:gravity="center"
android:layout_marginTop="16dp"

View file

@ -11,7 +11,7 @@
android:id="@+id/hostsRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="8dp"
android:paddingBottom="96dp"
android:clipToPadding="false"
android:clipChildren="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

View file

@ -37,6 +37,16 @@
android:layout_height="match_parent"
android:padding="8dp">
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/menuButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_overflow"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:padding="4dp"
android:background="?android:attr/selectableItemBackgroundBorderless"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/nameTextView"
android:layout_width="match_parent"

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/action_wakeup"
android:title="@string/action_wakeup"/>
<item android:id="@+id/action_edit"
android:title="@string/action_edit"/>
<item android:id="@+id/action_delete"
android:title="@string/action_delete" />
</menu>

View file

@ -45,7 +45,7 @@
<string name="alert_regist_duplicate">The console with MAC %s has already been registered. Should the previous record be overwritten?</string>
<string name="action_regist_overwrite">Overwrite</string>
<string name="action_regist_discard">Cancel</string>
<string name="title_add_manual">Add Console Manually</string>
<string name="title_edit_manual">Manual Console Entry</string>
<string name="action_add_manual_save">Save</string>
<string name="hint_add_manual_regist_host">Registered Console</string>
<string name="add_manual_regist_on_connect">Register on first Connection</string>
@ -59,8 +59,10 @@
<string name="preferences_log_verbose_title">Verbose Logging</string>
<string name="preferences_log_verbose_summary">Warning: This logs a LOT! Don\'t enable for regular use.</string>
<string name="alert_message_delete_registered_host">Are you sure you want to delete the registered console %s with ID %s?</string>
<string name="alert_action_delete_registered_host">Delete</string>
<string name="alert_action_keep_registered_host">Keep</string>
<string name="alert_message_delete_manual_host">Are you sure you want to delete the console entry for %s?</string>
<string name="action_keep">Keep</string>
<string name="action_delete">Delete</string>
<string name="action_edit">Edit</string>
<!-- Don't localize these -->
<string name="preferences_discovery_enabled_key">discovery_enabled</string>