Export Settings on Android

This commit is contained in:
Florian Märkl 2019-11-25 19:56:24 +01:00
commit e3ea1e40b2
No known key found for this signature in database
GPG key ID: 125BC8A5A6A1E857
10 changed files with 316 additions and 4 deletions

View file

@ -111,4 +111,6 @@ dependencies {
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version" implementation "androidx.room:room-ktx:$room_version"
implementation "androidx.room:room-rxjava2:$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"
} }

View file

@ -21,4 +21,71 @@
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
-dontobfuscate -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>;
}

View file

@ -17,6 +17,9 @@
package com.metallic.chiaki.common 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.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
@ -38,6 +41,14 @@ class MacAddress(v: Long)
buf.getLong(0) 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 val value: Long = v and 0xffffffffffff
override fun equals(other: Any?): Boolean = override fun equals(other: Any?): Boolean =
@ -56,4 +67,10 @@ class MacAddress(v: Long)
(value shr 0x20) and 0xff, (value shr 0x20) and 0xff,
(value shr 0x28) 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) }
} }

View file

@ -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)))
}
}
}

View file

@ -17,17 +17,24 @@
package com.metallic.chiaki.settings package com.metallic.chiaki.settings
import android.content.ClipData
import android.content.Intent
import android.content.res.Resources import android.content.res.Resources
import android.os.Bundle import android.os.Bundle
import android.text.InputType import android.text.InputType
import androidx.core.content.FileProvider
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import androidx.preference.* import androidx.preference.*
import com.metallic.chiaki.R 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.toLiveData
import com.metallic.chiaki.common.ext.viewModelFactory 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() class DataStore(val preferences: Preferences): PreferenceDataStore()
{ {
@ -76,11 +83,15 @@ class DataStore(val preferences: Preferences): PreferenceDataStore()
class SettingsFragment: PreferenceFragmentCompat(), TitleFragment class SettingsFragment: PreferenceFragmentCompat(), TitleFragment
{ {
private lateinit var viewModel: SettingsViewModel
private var disposable = CompositeDisposable()
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?)
{ {
val context = context ?: return val context = context ?: return
val viewModel = ViewModelProviders viewModel = ViewModelProviders
.of(this, viewModelFactory { SettingsViewModel(getDatabase(context), Preferences(context)) }) .of(this, viewModelFactory { SettingsViewModel(getDatabase(context), Preferences(context)) })
.get(SettingsViewModel::class.java) .get(SettingsViewModel::class.java)
@ -118,7 +129,28 @@ class SettingsFragment: PreferenceFragmentCompat(), TitleFragment
viewModel.registeredHostsCount.observe(this, Observer { viewModel.registeredHostsCount.observe(this, Observer {
registeredHostsPreference?.summary = getString(R.string.preferences_registered_hosts_summary, it) 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) 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()
{
}
} }

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="?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>

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="?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>

View file

@ -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_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="regist_pin_invalid">Please enter a valid %d-digit PIN</string>
<string name="action_share_log">Share Log</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_success">Regist successful.</string>
<string name="regist_info_failed">Regist failed.</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> <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="add_manual_regist_on_connect">Register on first Connection</string>
<string name="preferences_category_title_general">General</string> <string name="preferences_category_title_general">General</string>
<string name="preferences_category_title_stream">Stream</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_registered_hosts_title">Registered Consoles</string>
<string name="preferences_logs_title">Session Logs</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_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_registered_hosts_summary">Currently registered: %d</string>
<string name="preferences_resolution_title">Resolution</string> <string name="preferences_resolution_title">Resolution</string>
<string name="preferences_fps_title">FPS</string> <string name="preferences_fps_title">FPS</string>
@ -83,6 +89,8 @@
<string name="preferences_discovery_enabled_key">discovery_enabled</string> <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_on_screen_controls_enabled_key">on_screen_controls_enabled</string>
<string name="preferences_log_verbose_key">log_verbose</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_swap_cross_moon_key">swap_cross_moon</string>
<string name="preferences_resolution_key">stream_resolution</string> <string name="preferences_resolution_key">stream_resolution</string>
<string name="preferences_fps_key">stream_fps</string> <string name="preferences_fps_key">stream_fps</string>

View file

@ -3,4 +3,7 @@
<files-path <files-path
name="session_logs" name="session_logs"
path="session_logs/" /> <!-- must be in sync with LogManager --> 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> </paths>

View file

@ -53,4 +53,20 @@
app:title="@string/preferences_bitrate_title" app:title="@string/preferences_bitrate_title"
app:icon="@drawable/ic_bitrate"/> app:icon="@drawable/ic_bitrate"/>
</PreferenceCategory> </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> </PreferenceScreen>