mirror of
https://github.com/karasevm/PrivateDNSAndroid.git
synced 2025-06-28 20:29:56 +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 'com.google.android.material:material:1.12.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation 'com.google.guava:guava:33.1.0-android'
|
implementation 'com.google.guava:guava:33.1.0-android'
|
||||||
|
implementation 'com.google.code.gson:gson:2.11.0'
|
||||||
|
|
||||||
def shizuku_version = '13.1.5'
|
def shizuku_version = '13.1.5'
|
||||||
implementation "dev.rikka.shizuku:api:$shizuku_version"
|
implementation "dev.rikka.shizuku:api:$shizuku_version"
|
||||||
|
@ -60,6 +61,6 @@ dependencies {
|
||||||
compileOnly 'dev.rikka.hidden:stub:4.3.2'
|
compileOnly 'dev.rikka.hidden:stub:4.3.2'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
||||||
}
|
}
|
|
@ -1,7 +1,10 @@
|
||||||
package ru.karasevm.privatednstoggle
|
package ru.karasevm.privatednstoggle
|
||||||
|
|
||||||
import android.Manifest
|
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.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.pm.IPackageManager
|
import android.content.pm.IPackageManager
|
||||||
|
@ -15,11 +18,17 @@ import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper.DOWN
|
import androidx.recyclerview.widget.ItemTouchHelper.DOWN
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper.UP
|
import androidx.recyclerview.widget.ItemTouchHelper.UP
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
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 org.lsposed.hiddenapibypass.HiddenApiBypass
|
||||||
import rikka.shizuku.Shizuku
|
import rikka.shizuku.Shizuku
|
||||||
import rikka.shizuku.ShizukuBinderWrapper
|
import rikka.shizuku.ShizukuBinderWrapper
|
||||||
|
@ -28,6 +37,8 @@ import rikka.shizuku.SystemServiceHelper
|
||||||
import ru.karasevm.privatednstoggle.databinding.ActivityMainBinding
|
import ru.karasevm.privatednstoggle.databinding.ActivityMainBinding
|
||||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper
|
import ru.karasevm.privatednstoggle.utils.PreferenceHelper
|
||||||
import ru.karasevm.privatednstoggle.utils.PreferenceHelper.dns_servers
|
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,
|
class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogListener,
|
||||||
|
@ -38,10 +49,11 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
||||||
private var items = mutableListOf<String>()
|
private var items = mutableListOf<String>()
|
||||||
private lateinit var sharedPrefs: SharedPreferences
|
private lateinit var sharedPrefs: SharedPreferences
|
||||||
private lateinit var adapter: RecyclerAdapter
|
private lateinit var adapter: RecyclerAdapter
|
||||||
|
private lateinit var clipboard: ClipboardManager
|
||||||
|
private lateinit var gson: Gson
|
||||||
|
|
||||||
private val itemTouchHelper by lazy {
|
private val itemTouchHelper by lazy {
|
||||||
val simpleItemTouchCallback =
|
val simpleItemTouchCallback = object : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) {
|
||||||
object : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) {
|
|
||||||
|
|
||||||
override fun onMove(
|
override fun onMove(
|
||||||
recyclerView: RecyclerView,
|
recyclerView: RecyclerView,
|
||||||
|
@ -55,8 +67,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
||||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
|
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
|
||||||
|
|
||||||
override fun onSelectedChanged(
|
override fun onSelectedChanged(
|
||||||
viewHolder: RecyclerView.ViewHolder?,
|
viewHolder: RecyclerView.ViewHolder?, actionState: Int
|
||||||
actionState: Int
|
|
||||||
) {
|
) {
|
||||||
super.onSelectedChanged(viewHolder, actionState)
|
super.onSelectedChanged(viewHolder, actionState)
|
||||||
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
|
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
|
||||||
|
@ -70,8 +81,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clearView(
|
override fun clearView(
|
||||||
recyclerView: RecyclerView,
|
recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder
|
||||||
viewHolder: RecyclerView.ViewHolder
|
|
||||||
) {
|
) {
|
||||||
super.clearView(recyclerView, viewHolder)
|
super.clearView(recyclerView, viewHolder)
|
||||||
viewHolder.itemView.apply {
|
viewHolder.itemView.apply {
|
||||||
|
@ -98,6 +108,8 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
||||||
binding.recyclerView.layoutManager = linearLayoutManager
|
binding.recyclerView.layoutManager = linearLayoutManager
|
||||||
|
|
||||||
sharedPrefs = PreferenceHelper.defaultPreference(this)
|
sharedPrefs = PreferenceHelper.defaultPreference(this)
|
||||||
|
clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
gson = GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create()
|
||||||
|
|
||||||
items = sharedPrefs.dns_servers
|
items = sharedPrefs.dns_servers
|
||||||
if (items[0] == "") {
|
if (items[0] == "") {
|
||||||
|
@ -108,9 +120,9 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
||||||
val newFragment = DeleteServerDialogFragment(position)
|
val newFragment = DeleteServerDialogFragment(position)
|
||||||
newFragment.show(supportFragmentManager, "delete_server")
|
newFragment.show(supportFragmentManager, "delete_server")
|
||||||
}
|
}
|
||||||
adapter.onItemsChanged = { swapedItems ->
|
adapter.onItemsChanged = { swappedItems ->
|
||||||
items = swapedItems
|
items = swappedItems
|
||||||
sharedPrefs.dns_servers = swapedItems
|
sharedPrefs.dns_servers = swappedItems
|
||||||
}
|
}
|
||||||
adapter.onDragStart = { viewHolder ->
|
adapter.onDragStart = { viewHolder ->
|
||||||
itemTouchHelper.startDrag(viewHolder)
|
itemTouchHelper.startDrag(viewHolder)
|
||||||
|
@ -133,6 +145,54 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
||||||
true
|
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 -> {
|
R.id.options -> {
|
||||||
val newFragment = OptionsDialogFragment()
|
val newFragment = OptionsDialogFragment()
|
||||||
newFragment.show(supportFragmentManager, "options")
|
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() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
Shizuku.removeRequestPermissionResultListener(this::onRequestPermissionResult)
|
Shizuku.removeRequestPermissionResultListener(this::onRequestPermissionResult)
|
||||||
|
@ -224,9 +301,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
||||||
val pm = IPermissionManager.Stub.asInterface(binder)
|
val pm = IPermissionManager.Stub.asInterface(binder)
|
||||||
runCatching {
|
runCatching {
|
||||||
pm.grantRuntimePermission(
|
pm.grantRuntimePermission(
|
||||||
packageName,
|
packageName, Manifest.permission.WRITE_SECURE_SETTINGS, 0
|
||||||
Manifest.permission.WRITE_SECURE_SETTINGS,
|
|
||||||
0
|
|
||||||
)
|
)
|
||||||
}.onFailure { _ ->
|
}.onFailure { _ ->
|
||||||
if (Build.VERSION.SDK_INT >= 34) {
|
if (Build.VERSION.SDK_INT >= 34) {
|
||||||
|
@ -242,9 +317,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
||||||
val binder = ShizukuBinderWrapper(SystemServiceHelper.getSystemService("package"))
|
val binder = ShizukuBinderWrapper(SystemServiceHelper.getSystemService("package"))
|
||||||
val pm = IPackageManager.Stub.asInterface(binder)
|
val pm = IPackageManager.Stub.asInterface(binder)
|
||||||
pm.grantRuntimePermission(
|
pm.grantRuntimePermission(
|
||||||
packageName,
|
packageName, Manifest.permission.WRITE_SECURE_SETTINGS, 0
|
||||||
Manifest.permission.WRITE_SECURE_SETTINGS,
|
|
||||||
0
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.onFailure { e ->
|
}.onFailure { e ->
|
||||||
|
@ -252,8 +325,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
||||||
}.also {
|
}.also {
|
||||||
if (checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) {
|
if (checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
val browserIntent = Intent(
|
val browserIntent = Intent(
|
||||||
Intent.ACTION_VIEW,
|
Intent.ACTION_VIEW, Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/")
|
||||||
Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/")
|
|
||||||
)
|
)
|
||||||
startActivity(browserIntent)
|
startActivity(browserIntent)
|
||||||
finish()
|
finish()
|
||||||
|
@ -262,7 +334,6 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("PrivateApi")
|
|
||||||
override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) {
|
override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) {
|
||||||
val isGranted = grantResult == PackageManager.PERMISSION_GRANTED
|
val isGranted = grantResult == PackageManager.PERMISSION_GRANTED
|
||||||
|
|
||||||
|
@ -270,8 +341,7 @@ class MainActivity : AppCompatActivity(), AddServerDialogFragment.NoticeDialogLi
|
||||||
grantPermissionWithShizuku()
|
grantPermissionWithShizuku()
|
||||||
} else if (checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) {
|
} else if (checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
val browserIntent = Intent(
|
val browserIntent = Intent(
|
||||||
Intent.ACTION_VIEW,
|
Intent.ACTION_VIEW, Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/")
|
||||||
Uri.parse("https://karasevm.github.io/PrivateDNSAndroid/")
|
|
||||||
)
|
)
|
||||||
startActivity(browserIntent)
|
startActivity(browserIntent)
|
||||||
finish()
|
finish()
|
||||||
|
|
|
@ -26,7 +26,7 @@ object PreferenceHelper {
|
||||||
is Boolean -> putBoolean(key, value)
|
is Boolean -> putBoolean(key, value)
|
||||||
is Long -> putLong(key, value)
|
is Long -> putLong(key, value)
|
||||||
is Float -> putFloat(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"
|
app:showAsAction="ifRoom"
|
||||||
android:icon="@drawable/ic_baseline_settings_24"
|
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"
|
<item android:id="@+id/privacy_policy"
|
||||||
android:title="@string/menu_privacy_policy"
|
android:title="@string/menu_privacy_policy"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
|
@ -30,4 +30,10 @@
|
||||||
<string name="set_to_provider_toast">Private DNS set to %1$s</string>
|
<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="require_unlock_setting">Require unlocking the device to change server</string>
|
||||||
<string name="a11y_drag_handle">Drag handle</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>
|
</resources>
|
Loading…
Add table
Add a link
Reference in a new issue