mirror of
https://git.sr.ht/~thestr4ng3r/chiaki
synced 2025-08-14 18:57:07 -07:00
Export Settings on Android
This commit is contained in:
parent
3e2e12d002
commit
e3ea1e40b2
10 changed files with 316 additions and 4 deletions
|
@ -111,4 +111,6 @@ dependencies {
|
|||
kapt "androidx.room:room-compiler:$room_version"
|
||||
implementation "androidx.room:room-ktx:$room_version"
|
||||
implementation "androidx.room:room-rxjava2:$room_version"
|
||||
implementation "com.squareup.moshi:moshi:1.9.2"
|
||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.9.2"
|
||||
}
|
||||
|
|
69
android/app/proguard-rules.pro
vendored
69
android/app/proguard-rules.pro
vendored
|
@ -21,4 +21,71 @@
|
|||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-dontobfuscate
|
||||
-keep class com.metallic.chiaki.** { *; }
|
||||
-keep class com.metallic.chiaki.** { *; }
|
||||
|
||||
|
||||
##########################################
|
||||
# Moshi
|
||||
##########################################
|
||||
|
||||
# JSR 305 annotations are for embedding nullability information.
|
||||
-dontwarn javax.annotation.**
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@com.squareup.moshi.* <methods>;
|
||||
}
|
||||
|
||||
-keep @com.squareup.moshi.JsonQualifier interface *
|
||||
|
||||
# Enum field names are used by the integrated EnumJsonAdapter.
|
||||
# values() is synthesized by the Kotlin compiler and is used by EnumJsonAdapter indirectly
|
||||
# Annotate enums with @JsonClass(generateAdapter = false) to use them with Moshi.
|
||||
-keepclassmembers @com.squareup.moshi.JsonClass class * extends java.lang.Enum {
|
||||
<fields>;
|
||||
**[] values();
|
||||
}
|
||||
|
||||
# The name of @JsonClass types is used to look up the generated adapter.
|
||||
-keepnames @com.squareup.moshi.JsonClass class *
|
||||
|
||||
# Retain generated target class's synthetic defaults constructor and keep DefaultConstructorMarker's
|
||||
# name. We will look this up reflectively to invoke the type's constructor.
|
||||
#
|
||||
# We can't _just_ keep the defaults constructor because Proguard/R8's spec doesn't allow wildcard
|
||||
# matching preceding parameters.
|
||||
-keepnames class kotlin.jvm.internal.DefaultConstructorMarker
|
||||
-keepclassmembers @com.squareup.moshi.JsonClass @kotlin.Metadata class * {
|
||||
synthetic <init>(...);
|
||||
}
|
||||
|
||||
# Retain generated JsonAdapters if annotated type is retained.
|
||||
-if @com.squareup.moshi.JsonClass class *
|
||||
-keep class <1>JsonAdapter {
|
||||
<init>(...);
|
||||
<fields>;
|
||||
}
|
||||
-if @com.squareup.moshi.JsonClass class **$*
|
||||
-keep class <1>_<2>JsonAdapter {
|
||||
<init>(...);
|
||||
<fields>;
|
||||
}
|
||||
-if @com.squareup.moshi.JsonClass class **$*$*
|
||||
-keep class <1>_<2>_<3>JsonAdapter {
|
||||
<init>(...);
|
||||
<fields>;
|
||||
}
|
||||
-if @com.squareup.moshi.JsonClass class **$*$*$*
|
||||
-keep class <1>_<2>_<3>_<4>JsonAdapter {
|
||||
<init>(...);
|
||||
<fields>;
|
||||
}
|
||||
-if @com.squareup.moshi.JsonClass class **$*$*$*$*
|
||||
-keep class <1>_<2>_<3>_<4>_<5>JsonAdapter {
|
||||
<init>(...);
|
||||
<fields>;
|
||||
}
|
||||
-if @com.squareup.moshi.JsonClass class **$*$*$*$*$*
|
||||
-keep class <1>_<2>_<3>_<4>_<5>_<6>JsonAdapter {
|
||||
<init>(...);
|
||||
<fields>;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
|
||||
package com.metallic.chiaki.common
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import com.squareup.moshi.ToJson
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
|
@ -38,6 +41,14 @@ class MacAddress(v: Long)
|
|||
buf.getLong(0)
|
||||
})
|
||||
|
||||
constructor(string: String) : this(
|
||||
(Regex("([0-9A-Fa-f]{2})[:-]{5}([0-9A-Fa-f]{2})").matchEntire(string)
|
||||
?: throw IllegalArgumentException("Invalid MAC Address String"))
|
||||
.groupValues
|
||||
.subList(1, 7)
|
||||
.map { it.toByte() }
|
||||
.toByteArray())
|
||||
|
||||
val value: Long = v and 0xffffffffffff
|
||||
|
||||
override fun equals(other: Any?): Boolean =
|
||||
|
@ -56,4 +67,10 @@ class MacAddress(v: Long)
|
|||
(value shr 0x20) and 0xff,
|
||||
(value shr 0x28) and 0xff
|
||||
)
|
||||
}
|
||||
|
||||
class MacAddressJsonAdapter
|
||||
{
|
||||
@ToJson fun toJson(macAddress: MacAddress) = macAddress.toString()
|
||||
@FromJson fun fromJson(string: String) = try { MacAddress(string) } catch(e: IllegalArgumentException) { throw JsonDataException(e.message) }
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* This file is part of Chiaki.
|
||||
*
|
||||
* Chiaki 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, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Chiaki 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 Chiaki. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.metallic.chiaki.common
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ClipData
|
||||
import android.content.Intent
|
||||
import android.util.Base64
|
||||
import androidx.core.content.FileProvider
|
||||
import com.metallic.chiaki.R
|
||||
import com.squareup.moshi.*
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.rxkotlin.Singles
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import okio.Buffer
|
||||
import java.io.File
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class SerializedRegisteredHost(
|
||||
@Json(name = "ap_ssid") val apSsid: String?,
|
||||
@Json(name = "ap_bssid") val apBssid: String?,
|
||||
@Json(name = "ap_key") val apKey: String?,
|
||||
@Json(name = "ap_name") val apName: String?,
|
||||
@Json(name = "ps4_mac") val ps4Mac: MacAddress,
|
||||
@Json(name = "ps4_nickname") val ps4Nickname: String?,
|
||||
@Json(name = "rp_regist_key") val rpRegistKey: ByteArray,
|
||||
@Json(name = "rp_key_type") val rpKeyType: Int,
|
||||
@Json(name = "rp_key") val rpKey: ByteArray
|
||||
){
|
||||
constructor(registeredHost: RegisteredHost) : this(
|
||||
registeredHost.apSsid,
|
||||
registeredHost.apBssid,
|
||||
registeredHost.apKey,
|
||||
registeredHost.apName,
|
||||
registeredHost.ps4Mac,
|
||||
registeredHost.ps4Nickname,
|
||||
registeredHost.rpRegistKey,
|
||||
registeredHost.rpKeyType,
|
||||
registeredHost.rpKey
|
||||
)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class SerializedManualHost(
|
||||
@Json(name = "host") val host: String,
|
||||
@Json(name = "ps4_mac") val ps4Mac: MacAddress?
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SerializedSettings(
|
||||
@Json(name = "registered_hosts") val registeredHosts: List<SerializedRegisteredHost>,
|
||||
@Json(name = "manual_hosts") val manualHosts: List<SerializedManualHost>
|
||||
)
|
||||
{
|
||||
companion object
|
||||
{
|
||||
fun fromDatabase(db: AppDatabase) = Singles.zip(
|
||||
db.registeredHostDao().getAll().firstOrError(),
|
||||
db.manualHostDao().getAll().firstOrError()
|
||||
) { registeredHosts, manualHosts ->
|
||||
SerializedSettings(
|
||||
registeredHosts.map { SerializedRegisteredHost(it) },
|
||||
manualHosts.map { manualHost ->
|
||||
SerializedManualHost(
|
||||
manualHost.host,
|
||||
manualHost.registeredHost?.let { registeredHostId ->
|
||||
registeredHosts.firstOrNull { it.id == registeredHostId }
|
||||
}?.ps4Mac
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ByteArrayJsonAdapter
|
||||
{
|
||||
@ToJson fun toJson(byteArray: ByteArray) = Base64.encodeToString(byteArray, Base64.NO_WRAP)
|
||||
@FromJson fun fromJson(string: String) = Base64.decode(string, Base64.DEFAULT)
|
||||
}
|
||||
|
||||
private fun moshi() =
|
||||
Moshi.Builder()
|
||||
.add(MacAddressJsonAdapter())
|
||||
.add(ByteArrayJsonAdapter())
|
||||
.build()
|
||||
|
||||
private const val KEY_FORMAT = "format"
|
||||
private const val FORMAT = "chiaki-settings"
|
||||
private const val KEY_VERSION = "version"
|
||||
private const val VERSION = "1"
|
||||
private const val KEY_SETTINGS = "settings"
|
||||
|
||||
fun exportAllSettings(db: AppDatabase) = SerializedSettings.fromDatabase(db)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.map {
|
||||
val buffer = Buffer()
|
||||
val writer = JsonWriter.of(buffer)
|
||||
val adapter = moshi()
|
||||
.adapter(SerializedSettings::class.java)
|
||||
.serializeNulls()
|
||||
writer.indent = " "
|
||||
writer.
|
||||
beginObject()
|
||||
.name(KEY_FORMAT).value(FORMAT)
|
||||
.name(KEY_VERSION).value(VERSION)
|
||||
writer.name(KEY_SETTINGS)
|
||||
adapter.toJson(writer, it)
|
||||
writer.endObject()
|
||||
buffer.readUtf8()
|
||||
}
|
||||
|
||||
fun exportAndShareAllSettings(db: AppDatabase, activity: Activity): Disposable
|
||||
{
|
||||
val dir = File(activity.cacheDir, "export_settings")
|
||||
dir.mkdirs()
|
||||
val file = File(dir, "chiaki-settings.json")
|
||||
return exportAllSettings(db)
|
||||
.map {
|
||||
file.writeText(it, Charsets.UTF_8)
|
||||
file
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { it: File ->
|
||||
val uri = FileProvider.getUriForFile(activity, fileProviderAuthority, file)
|
||||
Intent(Intent.ACTION_SEND).also {
|
||||
it.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
it.type = "application/json"
|
||||
it.putExtra(Intent.EXTRA_STREAM, uri)
|
||||
it.clipData = ClipData.newRawUri("", uri)
|
||||
activity.startActivity(Intent.createChooser(it, activity.getString(R.string.action_share_log)))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,17 +17,24 @@
|
|||
|
||||
package com.metallic.chiaki.settings
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.Intent
|
||||
import android.content.res.Resources
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.preference.*
|
||||
import com.metallic.chiaki.R
|
||||
import com.metallic.chiaki.common.Preferences
|
||||
import com.metallic.chiaki.common.*
|
||||
import com.metallic.chiaki.common.ext.toLiveData
|
||||
import com.metallic.chiaki.common.ext.viewModelFactory
|
||||
import com.metallic.chiaki.common.getDatabase
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.rxkotlin.addTo
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import java.io.File
|
||||
|
||||
class DataStore(val preferences: Preferences): PreferenceDataStore()
|
||||
{
|
||||
|
@ -76,11 +83,15 @@ class DataStore(val preferences: Preferences): PreferenceDataStore()
|
|||
|
||||
class SettingsFragment: PreferenceFragmentCompat(), TitleFragment
|
||||
{
|
||||
private lateinit var viewModel: SettingsViewModel
|
||||
|
||||
private var disposable = CompositeDisposable()
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?)
|
||||
{
|
||||
val context = context ?: return
|
||||
|
||||
val viewModel = ViewModelProviders
|
||||
viewModel = ViewModelProviders
|
||||
.of(this, viewModelFactory { SettingsViewModel(getDatabase(context), Preferences(context)) })
|
||||
.get(SettingsViewModel::class.java)
|
||||
|
||||
|
@ -118,7 +129,28 @@ class SettingsFragment: PreferenceFragmentCompat(), TitleFragment
|
|||
viewModel.registeredHostsCount.observe(this, Observer {
|
||||
registeredHostsPreference?.summary = getString(R.string.preferences_registered_hosts_summary, it)
|
||||
})
|
||||
|
||||
preferenceScreen.findPreference<Preference>(getString(R.string.preferences_export_settings_key))?.setOnPreferenceClickListener { exportSettings(); true }
|
||||
preferenceScreen.findPreference<Preference>(getString(R.string.preferences_import_settings_key))?.setOnPreferenceClickListener { importSettings(); true }
|
||||
}
|
||||
|
||||
override fun onDestroy()
|
||||
{
|
||||
super.onDestroy()
|
||||
disposable.dispose()
|
||||
}
|
||||
|
||||
override fun getTitle(resources: Resources): String = resources.getString(R.string.title_settings)
|
||||
|
||||
private fun exportSettings()
|
||||
{
|
||||
val activity = activity ?: return
|
||||
disposable.clear()
|
||||
exportAndShareAllSettings(viewModel.database, activity).addTo(disposable)
|
||||
}
|
||||
|
||||
private fun importSettings()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
9
android/app/src/main/res/drawable/ic_export.xml
Normal file
9
android/app/src/main/res/drawable/ic_export.xml
Normal 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="?android:attr/textColorPrimary"
|
||||
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
|
||||
</vector>
|
9
android/app/src/main/res/drawable/ic_import.xml
Normal file
9
android/app/src/main/res/drawable/ic_import.xml
Normal 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="?android:attr/textColorPrimary"
|
||||
android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM17,13l-5,5 -5,-5h3V9h4v4h3z"/>
|
||||
</vector>
|
|
@ -40,6 +40,7 @@
|
|||
<string name="regist_psn_account_id_invalid">Please enter a valid 8-byte Account ID in Base64</string>
|
||||
<string name="regist_pin_invalid">Please enter a valid %d-digit PIN</string>
|
||||
<string name="action_share_log">Share Log</string>
|
||||
<string name="action_export_settings">Export Settings</string>
|
||||
<string name="regist_info_success">Regist successful.</string>
|
||||
<string name="regist_info_failed">Regist failed.</string>
|
||||
<string name="alert_regist_duplicate">The console with MAC %s has already been registered. Should the previous record be overwritten?</string>
|
||||
|
@ -51,9 +52,14 @@
|
|||
<string name="add_manual_regist_on_connect">Register on first Connection</string>
|
||||
<string name="preferences_category_title_general">General</string>
|
||||
<string name="preferences_category_title_stream">Stream</string>
|
||||
<string name="preferences_category_title_export">Export</string>
|
||||
<string name="preferences_registered_hosts_title">Registered Consoles</string>
|
||||
<string name="preferences_logs_title">Session Logs</string>
|
||||
<string name="preferences_logs_summary">Collected log files from previous sessions for debugging</string>
|
||||
<string name="preferences_export_settings_title">Export Settings</string>
|
||||
<string name="preferences_export_settings_summary">Warning: These resulting file can contain your secret Remote Play keys! Do not share them.</string>
|
||||
<string name="preferences_import_settings_title">Import Settings</string>
|
||||
<string name="preferences_import_settings_summary">Import Settings from JSON</string>
|
||||
<string name="preferences_registered_hosts_summary">Currently registered: %d</string>
|
||||
<string name="preferences_resolution_title">Resolution</string>
|
||||
<string name="preferences_fps_title">FPS</string>
|
||||
|
@ -83,6 +89,8 @@
|
|||
<string name="preferences_discovery_enabled_key">discovery_enabled</string>
|
||||
<string name="preferences_on_screen_controls_enabled_key">on_screen_controls_enabled</string>
|
||||
<string name="preferences_log_verbose_key">log_verbose</string>
|
||||
<string name="preferences_import_settings_key">import_settings</string>
|
||||
<string name="preferences_export_settings_key">export_settings</string>
|
||||
<string name="preferences_swap_cross_moon_key">swap_cross_moon</string>
|
||||
<string name="preferences_resolution_key">stream_resolution</string>
|
||||
<string name="preferences_fps_key">stream_fps</string>
|
||||
|
|
|
@ -3,4 +3,7 @@
|
|||
<files-path
|
||||
name="session_logs"
|
||||
path="session_logs/" /> <!-- must be in sync with LogManager -->
|
||||
<cache-path
|
||||
name="export_settings"
|
||||
path="export_settings/"/> <!-- must be in sync with SerializedSettings.kt -->
|
||||
</paths>
|
|
@ -53,4 +53,20 @@
|
|||
app:title="@string/preferences_bitrate_title"
|
||||
app:icon="@drawable/ic_bitrate"/>
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
app:key="category_export"
|
||||
app:title="@string/preferences_category_title_export">
|
||||
<Preference
|
||||
app:key="@string/preferences_export_settings_key"
|
||||
app:summary="@string/preferences_export_settings_summary"
|
||||
app:title="@string/preferences_export_settings_title"
|
||||
app:icon="@drawable/ic_export"/>
|
||||
|
||||
<Preference
|
||||
app:key="@string/preferences_import_settings_key"
|
||||
app:summary="@string/preferences_import_settings_summary"
|
||||
app:title="@string/preferences_import_settings_title"
|
||||
app:icon="@drawable/ic_import"/>
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
Loading…
Add table
Add a link
Reference in a new issue