mirror of
https://github.com/karasevm/PrivateDNSAndroid.git
synced 2025-06-28 12:19:57 +00:00
Implement settings export/import (#26)
This commit is contained in:
parent
6caa1432aa
commit
3a697e32e3
5 changed files with 156 additions and 54 deletions
|
@ -51,6 +51,7 @@ dependencies {
|
|||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'com.google.guava:guava:33.1.0-android'
|
||||
implementation 'com.google.code.gson:gson:2.11.0'
|
||||
|
||||
def shizuku_version = '13.1.5'
|
||||
implementation "dev.rikka.shizuku:api:$shizuku_version"
|
||||
|
@ -60,6 +61,6 @@ dependencies {
|
|||
compileOnly 'dev.rikka.hidden:stub:4.3.2'
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
package ru.karasevm.privatednstoggle
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ClipData
|
||||
import android.content.ClipDescription.MIMETYPE_TEXT_PLAIN
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.IPackageManager
|
||||
|
@ -15,11 +18,17 @@ import android.util.Log
|
|||
import android.view.Menu
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.ItemTouchHelper.DOWN
|
||||
import androidx.recyclerview.widget.ItemTouchHelper.UP
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonSyntaxException
|
||||
import com.google.gson.ToNumberPolicy
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
||||
import rikka.shizuku.Shizuku
|
||||
import rikka.shizuku.ShizukuBinderWrapper
|
||||
|
@ -28,6 +37,8 @@ import rikka.shizuku.SystemServiceHelper
|
|||
import ru.karasevm.privatednstoggle.databinding.ActivityMainBinding
|
||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper
|
||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.dns_servers
|
||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.export
|
||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.import
|
||||
|
||||
|
||||
class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogListener,
|
||||
|
@ -38,10 +49,11 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
|||
private var items = mutableListOf<String>()
|
||||
private lateinit var sharedPrefs: SharedPreferences
|
||||
private lateinit var adapter: RecyclerAdapter
|
||||
private lateinit var clipboard: ClipboardManager
|
||||
private lateinit var gson: Gson
|
||||
|
||||
private val itemTouchHelper by lazy {
|
||||
val simpleItemTouchCallback =
|
||||
object : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) {
|
||||
val simpleItemTouchCallback = object : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) {
|
||||
|
||||
override fun onMove(
|
||||
recyclerView: RecyclerView,
|
||||
|
@ -55,8 +67,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
|||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
|
||||
|
||||
override fun onSelectedChanged(
|
||||
viewHolder: RecyclerView.ViewHolder?,
|
||||
actionState: Int
|
||||
viewHolder: RecyclerView.ViewHolder?, actionState: Int
|
||||
) {
|
||||
super.onSelectedChanged(viewHolder, actionState)
|
||||
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
|
||||
|
@ -70,8 +81,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
|||
}
|
||||
|
||||
override fun clearView(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder
|
||||
recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder
|
||||
) {
|
||||
super.clearView(recyclerView, viewHolder)
|
||||
viewHolder.itemView.apply {
|
||||
|
@ -98,6 +108,8 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
|||
binding.recyclerView.layoutManager = linearLayoutManager
|
||||
|
||||
sharedPrefs = PreferenceHelper.defaultPreference(this)
|
||||
clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
gson = GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create()
|
||||
|
||||
items = sharedPrefs.dns_servers
|
||||
if (items[0] == "") {
|
||||
|
@ -108,9 +120,9 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
|||
val newFragment = DeleteServerDialogFragment(position)
|
||||
newFragment.show(supportFragmentManager, "delete_server")
|
||||
}
|
||||
adapter.onItemsChanged = { swapedItems ->
|
||||
items = swapedItems
|
||||
sharedPrefs.dns_servers = swapedItems
|
||||
adapter.onItemsChanged = { swappedItems ->
|
||||
items = swappedItems
|
||||
sharedPrefs.dns_servers = swappedItems
|
||||
}
|
||||
adapter.onDragStart = { viewHolder ->
|
||||
itemTouchHelper.startDrag(viewHolder)
|
||||
|
@ -133,6 +145,54 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
|||
true
|
||||
}
|
||||
|
||||
R.id.export_settings -> {
|
||||
val data = sharedPrefs.export()
|
||||
val jsonData = gson.toJson(data)
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("", jsonData))
|
||||
// Only show a toast for Android 12 and lower.
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) Toast.makeText(
|
||||
this, getString(R.string.copy_success), Toast.LENGTH_SHORT
|
||||
).show()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.import_settings -> {
|
||||
val clipData = clipboard.primaryClip?.getItemAt(0)
|
||||
val textData = clipData?.text
|
||||
|
||||
if (textData != null) {
|
||||
runCatching {
|
||||
val jsonData = textData.toString()
|
||||
val objectType = object : TypeToken<Map<String, Any>>() {}.type
|
||||
val data: Map<String, Any> = gson.fromJson(jsonData, objectType)
|
||||
sharedPrefs.import(data)
|
||||
}.onSuccess {
|
||||
Toast.makeText(
|
||||
this, getString(R.string.import_success), Toast.LENGTH_SHORT
|
||||
).show()
|
||||
ActivityCompat.recreate(this)
|
||||
}.onFailure { exception ->
|
||||
Log.e("IMPORT", "Import failed", exception)
|
||||
when (exception) {
|
||||
is JsonSyntaxException -> {
|
||||
Toast.makeText(
|
||||
this,
|
||||
getString(R.string.import_failure_json),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
else -> {
|
||||
Toast.makeText(
|
||||
this, getString(R.string.import_failure), Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.options -> {
|
||||
val newFragment = OptionsDialogFragment()
|
||||
newFragment.show(supportFragmentManager, "options")
|
||||
|
@ -184,6 +244,23 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
|||
}
|
||||
}
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
if (!hasFocus) {
|
||||
// Gets the ID of the "paste" menu item.
|
||||
val pasteItem = binding.topAppBar.menu.findItem(R.id.import_settings)
|
||||
|
||||
// If the clipboard doesn't contain data, disable the paste menu item.
|
||||
// If it does contain data, decide whether you can handle the data.
|
||||
pasteItem.isEnabled = when {
|
||||
!clipboard.hasPrimaryClip() -> false
|
||||
!(clipboard.primaryClipDescription?.hasMimeType(MIMETYPE_TEXT_PLAIN))!! -> false
|
||||
else -> true
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Shizuku.removeRequestPermissionResultListener(this::onRequestPermissionResult)
|
||||
|
@ -224,9 +301,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
|||
val pm = IPermissionManager.Stub.asInterface(binder)
|
||||
runCatching {
|
||||
pm.grantRuntimePermission(
|
||||
packageName,
|
||||
Manifest.permission.WRITE_SECURE_SETTINGS,
|
||||
0
|
||||
packageName, Manifest.permission.WRITE_SECURE_SETTINGS, 0
|
||||
)
|
||||
}.onFailure { _ ->
|
||||
if (Build.VERSION.SDK_INT >= 34) {
|
||||
|
@ -242,9 +317,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
|||
val binder = ShizukuBinderWrapper(SystemServiceHelper.getSystemService("package"))
|
||||
val pm = IPackageManager.Stub.asInterface(binder)
|
||||
pm.grantRuntimePermission(
|
||||
packageName,
|
||||
Manifest.permission.WRITE_SECURE_SETTINGS,
|
||||
0
|
||||
packageName, Manifest.permission.WRITE_SECURE_SETTINGS, 0
|
||||
)
|
||||
}
|
||||
}.onFailure { e ->
|
||||
|
@ -252,8 +325,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
|||
}.also {
|
||||
if (checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) {
|
||||
val browserIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/")
|
||||
Intent.ACTION_VIEW, Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/")
|
||||
)
|
||||
startActivity(browserIntent)
|
||||
finish()
|
||||
|
@ -262,7 +334,6 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
|||
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) {
|
||||
val isGranted = grantResult == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
|
@ -270,8 +341,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
|||
grantPermissionWithShizuku()
|
||||
} else if (checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) {
|
||||
val browserIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/")
|
||||
Intent.ACTION_VIEW, Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/")
|
||||
)
|
||||
startActivity(browserIntent)
|
||||
finish()
|
||||
|
|
|
@ -26,7 +26,7 @@ object PreferenceHelper {
|
|||
is Boolean -> putBoolean(key, value)
|
||||
is Long -> putLong(key, value)
|
||||
is Float -> putFloat(key, value)
|
||||
else -> error("Only primitive types can be stored in SharedPreferences")
|
||||
else -> error("Only primitive types can be stored in SharedPreferences, got ${value.javaClass}")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,4 +54,23 @@ object PreferenceHelper {
|
|||
}
|
||||
}
|
||||
|
||||
// export all the preferences
|
||||
fun SharedPreferences.export() = mapOf(
|
||||
DNS_SERVERS to getString(DNS_SERVERS, ""),
|
||||
AUTO_MODE to autoMode,
|
||||
REQUIRE_UNLOCK to requireUnlock
|
||||
)
|
||||
|
||||
// import all the preferences
|
||||
fun SharedPreferences.import(map: Map<String, Any>) {
|
||||
editMe {
|
||||
map.forEach { (key, value) ->
|
||||
if (value is Number) {
|
||||
it.put(key to value.toInt())
|
||||
return@forEach
|
||||
}
|
||||
it.put(key to value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,12 @@
|
|||
app:showAsAction="ifRoom"
|
||||
android:icon="@drawable/ic_baseline_settings_24"
|
||||
/>
|
||||
<item android:id="@+id/import_settings"
|
||||
android:title="@string/menu_import"
|
||||
app:showAsAction="never" />
|
||||
<item android:id="@+id/export_settings"
|
||||
android:title="@string/menu_export"
|
||||
app:showAsAction="never" />
|
||||
<item android:id="@+id/privacy_policy"
|
||||
android:title="@string/menu_privacy_policy"
|
||||
app:showAsAction="never" />
|
||||
|
|
|
@ -30,4 +30,10 @@
|
|||
<string name="set_to_provider_toast">Private DNS set to %1$s</string>
|
||||
<string name="require_unlock_setting">Require unlocking the device to change server</string>
|
||||
<string name="a11y_drag_handle">Drag handle</string>
|
||||
<string name="menu_import">Import from clipboard</string>
|
||||
<string name="menu_export">Export to clipboard</string>
|
||||
<string name="import_success">Imported</string>
|
||||
<string name="import_failure">Import failed</string>
|
||||
<string name="import_failure_json">Import failed, malformed JSON</string>
|
||||
<string name="copy_success">Copied</string>
|
||||
</resources>
|
Loading…
Add table
Add a link
Reference in a new issue