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"
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"
}

View file

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

View file

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

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
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()
{
}
}

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_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>

View file

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

View file

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