From fe0b3fdce37ad360d6b749fcfa3d2e26ff06b604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 30 Jul 2023 21:16:05 +0800 Subject: [PATCH] Add profile sharing --- app/src/main/AndroidManifest.xml | 33 +++++- .../nekohasekai/sfa/bg/AppChangeReceiver.kt | 4 +- .../java/io/nekohasekai/sfa/bg/BoxService.kt | 5 +- .../nekohasekai/sfa/bg/UpdateProfileWork.kt | 8 +- .../sfa/constant/PerAppProxyUpdateType.kt | 1 + .../{Profiles.kt => ProfileManager.kt} | 47 +++++++- .../io/nekohasekai/sfa/database/Settings.kt | 2 +- .../java/io/nekohasekai/sfa/ktx/Shares.kt | 45 ++++++++ .../io/nekohasekai/sfa/ui/MainActivity.kt | 107 ++++++++++++++---- .../sfa/ui/main/ConfigurationFragment.kt | 39 +++---- .../sfa/ui/main/DashboardFragment.kt | 12 +- .../sfa/ui/main/SettingsFragment.kt | 4 +- .../sfa/ui/profile/EditProfileActivity.kt | 24 +++- .../ui/profile/EditProfileContentActivity.kt | 4 +- .../sfa/ui/profile/NewProfileActivity.kt | 8 +- .../PerAppProxyActivity.kt | 5 +- .../ProfileOverrideActivity.kt} | 9 +- .../res/layout/activity_config_override.xml | 2 +- .../main/res/layout/activity_edit_profile.xml | 8 ++ app/src/main/res/layout/fragment_settings.xml | 6 +- app/src/main/res/values/strings.xml | 12 +- app/src/main/res/xml/cache_paths.xml | 4 + 22 files changed, 302 insertions(+), 87 deletions(-) rename app/src/main/java/io/nekohasekai/sfa/database/{Profiles.kt => ProfileManager.kt} (53%) create mode 100644 app/src/main/java/io/nekohasekai/sfa/ktx/Shares.kt rename app/src/main/java/io/nekohasekai/sfa/ui/{configoverride => profileoverride}/PerAppProxyActivity.kt (99%) rename app/src/main/java/io/nekohasekai/sfa/ui/{configoverride/ConfigOverrideActivity.kt => profileoverride/ProfileOverrideActivity.kt} (89%) create mode 100644 app/src/main/res/xml/cache_paths.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2c9807b..8e51c2e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -40,7 +40,7 @@ - + @@ -52,6 +52,23 @@ + + + + + + + + + + + + + + + + + @@ -79,10 +96,10 @@ android:name="io.nekohasekai.sfa.ui.profile.EditProfileContentActivity" android:exported="false" /> + + + + \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/AppChangeReceiver.kt b/app/src/main/java/io/nekohasekai/sfa/bg/AppChangeReceiver.kt index fc4d814..f09978e 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/AppChangeReceiver.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/AppChangeReceiver.kt @@ -5,9 +5,7 @@ import android.content.Context import android.content.Intent import android.util.Log import io.nekohasekai.sfa.database.Settings -import io.nekohasekai.sfa.ui.configoverride.PerAppProxyActivity -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import io.nekohasekai.sfa.ui.profileoverride.PerAppProxyActivity class AppChangeReceiver : BroadcastReceiver() { diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/BoxService.kt b/app/src/main/java/io/nekohasekai/sfa/bg/BoxService.kt index 717c4f1..4efad9e 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/BoxService.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/BoxService.kt @@ -20,7 +20,7 @@ import io.nekohasekai.sfa.Application import io.nekohasekai.sfa.constant.Action import io.nekohasekai.sfa.constant.Alert import io.nekohasekai.sfa.constant.Status -import io.nekohasekai.sfa.database.Profiles +import io.nekohasekai.sfa.database.ProfileManager import io.nekohasekai.sfa.database.Settings import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -92,6 +92,7 @@ class BoxService( Action.SERVICE_CLOSE -> { stopService() } + Action.SERVICE_RELOAD -> { serviceReload() } @@ -114,7 +115,7 @@ class BoxService( return } - val profile = Profiles.get(selectedProfileId) + val profile = ProfileManager.get(selectedProfileId) if (profile == null) { stopAndAlert(Alert.EmptyConfiguration) return diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/UpdateProfileWork.kt b/app/src/main/java/io/nekohasekai/sfa/bg/UpdateProfileWork.kt index efe2ad9..bbe78d6 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/UpdateProfileWork.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/UpdateProfileWork.kt @@ -10,7 +10,7 @@ import androidx.work.WorkManager import androidx.work.WorkerParameters import io.nekohasekai.libbox.Libbox import io.nekohasekai.sfa.Application -import io.nekohasekai.sfa.database.Profiles +import io.nekohasekai.sfa.database.ProfileManager import io.nekohasekai.sfa.database.TypedProfile import io.nekohasekai.sfa.utils.HTTPClient import java.io.File @@ -33,7 +33,7 @@ class UpdateProfileWork { private suspend fun reconfigureUpdater0() { WorkManager.getInstance(Application.application).cancelUniqueWork(WORK_NAME) - val remoteProfiles = Profiles.list() + val remoteProfiles = ProfileManager.list() .filter { it.typed.type == TypedProfile.Type.Remote && it.typed.autoUpdate } if (remoteProfiles.isEmpty()) return @@ -62,7 +62,7 @@ class UpdateProfileWork { appContext: Context, params: WorkerParameters ) : CoroutineWorker(appContext, params) { override suspend fun doWork(): Result { - val remoteProfiles = Profiles.list() + val remoteProfiles = ProfileManager.list() .filter { it.typed.type == TypedProfile.Type.Remote && it.typed.autoUpdate } if (remoteProfiles.isEmpty()) return Result.success() val httpClient = HTTPClient() @@ -73,7 +73,7 @@ class UpdateProfileWork { Libbox.checkConfig(content) File(profile.typed.path).writeText(content) profile.typed.lastUpdated = Date() - Profiles.update(profile) + ProfileManager.update(profile) } catch (e: Exception) { Log.e("UpdateProfileWork", "error when updating profile ${profile.name}", e) success = false diff --git a/app/src/main/java/io/nekohasekai/sfa/constant/PerAppProxyUpdateType.kt b/app/src/main/java/io/nekohasekai/sfa/constant/PerAppProxyUpdateType.kt index 095cec0..aed1f19 100644 --- a/app/src/main/java/io/nekohasekai/sfa/constant/PerAppProxyUpdateType.kt +++ b/app/src/main/java/io/nekohasekai/sfa/constant/PerAppProxyUpdateType.kt @@ -10,6 +10,7 @@ enum class PerAppProxyUpdateType { Select -> Settings.PER_APP_PROXY_INCLUDE Deselect -> Settings.PER_APP_PROXY_EXCLUDE } + companion object { fun valueOf(value: Int): PerAppProxyUpdateType = when (value) { Settings.PER_APP_PROXY_DISABLED -> Disabled diff --git a/app/src/main/java/io/nekohasekai/sfa/database/Profiles.kt b/app/src/main/java/io/nekohasekai/sfa/database/ProfileManager.kt similarity index 53% rename from app/src/main/java/io/nekohasekai/sfa/database/Profiles.kt rename to app/src/main/java/io/nekohasekai/sfa/database/ProfileManager.kt index 641d4d2..c068906 100644 --- a/app/src/main/java/io/nekohasekai/sfa/database/Profiles.kt +++ b/app/src/main/java/io/nekohasekai/sfa/database/ProfileManager.kt @@ -7,7 +7,17 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @Suppress("RedundantSuspendModifier") -object Profiles { +object ProfileManager { + + private val callbacks = mutableListOf<() -> Unit>() + + fun registerCallback(callback: () -> Unit) { + callbacks.add(callback) + } + + fun unregisterCallback(callback: () -> Unit) { + callbacks.remove(callback) + } private val instance by lazy { Application.application.getDatabasePath(Path.PROFILES_DATABASE_PATH).parentFile?.mkdirs() @@ -27,23 +37,50 @@ object Profiles { suspend fun create(profile: Profile): Profile { profile.id = instance.profileDao().insert(profile) + for (callback in callbacks.toList()) { + callback() + } return profile } suspend fun update(profile: Profile): Int { - return instance.profileDao().update(profile) + try { + return instance.profileDao().update(profile) + } finally { + for (callback in callbacks.toList()) { + callback() + } + } } suspend fun update(profiles: List): Int { - return instance.profileDao().update(profiles) + try { + return instance.profileDao().update(profiles) + } finally { + for (callback in callbacks.toList()) { + callback() + } + } } suspend fun delete(profile: Profile): Int { - return instance.profileDao().delete(profile) + try { + return instance.profileDao().delete(profile) + } finally { + for (callback in callbacks.toList()) { + callback() + } + } } suspend fun delete(profiles: List): Int { - return instance.profileDao().delete(profiles) + try { + return instance.profileDao().delete(profiles) + } finally { + for (callback in callbacks.toList()) { + callback() + } + } } suspend fun list(): List { diff --git a/app/src/main/java/io/nekohasekai/sfa/database/Settings.kt b/app/src/main/java/io/nekohasekai/sfa/database/Settings.kt index da2b15a..3c9fd90 100644 --- a/app/src/main/java/io/nekohasekai/sfa/database/Settings.kt +++ b/app/src/main/java/io/nekohasekai/sfa/database/Settings.kt @@ -80,7 +80,7 @@ object Settings { private suspend fun needVPNService(): Boolean { val selectedProfileId = selectedProfile if (selectedProfileId == -1L) return false - val profile = Profiles.get(selectedProfile) ?: return false + val profile = ProfileManager.get(selectedProfile) ?: return false val content = JSONObject(File(profile.typed.path).readText()) val inbounds = content.getJSONArray("inbounds") for (index in 0 until inbounds.length()) { diff --git a/app/src/main/java/io/nekohasekai/sfa/ktx/Shares.kt b/app/src/main/java/io/nekohasekai/sfa/ktx/Shares.kt new file mode 100644 index 0000000..d1bf971 --- /dev/null +++ b/app/src/main/java/io/nekohasekai/sfa/ktx/Shares.kt @@ -0,0 +1,45 @@ +package io.nekohasekai.sfa.ktx + +import android.content.Context +import android.content.Intent +import androidx.core.content.FileProvider +import com.google.android.material.R +import io.nekohasekai.libbox.ProfileContent +import io.nekohasekai.sfa.database.Profile +import io.nekohasekai.sfa.database.TypedProfile +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File + +suspend fun Context.shareProfile(profile: Profile) { + val content = ProfileContent() + content.name = profile.name + when (profile.typed.type) { + TypedProfile.Type.Local -> { + content.type = io.nekohasekai.libbox.Libbox.ProfileTypeLocal + } + + TypedProfile.Type.Remote -> { + content.type = io.nekohasekai.libbox.Libbox.ProfileTypeRemote + } + } + content.config = File(profile.typed.path).readText() + content.remotePath = profile.typed.remoteURL + content.autoUpdate = profile.typed.autoUpdate + content.lastUpdated = profile.typed.lastUpdated.time + + val configDirectory = File(cacheDir, "share").also { it.mkdirs() } + val profileFile = File(configDirectory, "${profile.name}.bpf") + profileFile.writeBytes(content.encode()) + val uri = FileProvider.getUriForFile(this, "$packageName.cache", profileFile) + withContext(Dispatchers.Main) { + startActivity( + Intent.createChooser( + Intent(Intent.ACTION_SEND).setType("application/octet-stream") + .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .putExtra(Intent.EXTRA_STREAM, uri), + getString(R.string.abc_shareactionprovider_share_with) + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt index 7a63be8..3819e9c 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt @@ -27,6 +27,7 @@ import com.microsoft.appcenter.distribute.ReleaseDetails import com.microsoft.appcenter.distribute.UpdateAction import com.microsoft.appcenter.utils.AppNameHelper import io.nekohasekai.libbox.Libbox +import io.nekohasekai.libbox.ProfileContent import io.nekohasekai.sfa.Application import io.nekohasekai.sfa.BuildConfig import io.nekohasekai.sfa.R @@ -35,7 +36,10 @@ import io.nekohasekai.sfa.bg.ServiceNotification import io.nekohasekai.sfa.constant.Alert import io.nekohasekai.sfa.constant.ServiceMode import io.nekohasekai.sfa.constant.Status +import io.nekohasekai.sfa.database.Profile +import io.nekohasekai.sfa.database.ProfileManager import io.nekohasekai.sfa.database.Settings +import io.nekohasekai.sfa.database.TypedProfile import io.nekohasekai.sfa.databinding.ActivityMainBinding import io.nekohasekai.sfa.ktx.errorDialogBuilder import io.nekohasekai.sfa.ui.profile.NewProfileActivity @@ -44,6 +48,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.io.File +import java.util.Date import java.util.LinkedList class MainActivity : AbstractActivity(), ServiceConnection.Callback, DistributeListener { @@ -66,6 +72,7 @@ class MainActivity : AbstractActivity(), ServiceConnection.Callback, DistributeL setContentView(binding.root) val navController = findNavController(R.id.nav_host_fragment_activity_my) + navController.navigate(R.id.navigation_dashboard) val appBarConfiguration = AppBarConfiguration( setOf( @@ -85,26 +92,88 @@ class MainActivity : AbstractActivity(), ServiceConnection.Callback, DistributeL override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) val uri = intent.data ?: return - if (uri.scheme != "sing-box" || uri.host != "import-remote-profile") { - return - } - val profile = try { - Libbox.parseRemoteProfileImportLink(uri.toString()) - } catch (e: Exception) { - errorDialogBuilder(e).show() - return - } - MaterialAlertDialogBuilder(this) - .setTitle(R.string.import_remote_profile) - .setMessage(getString(R.string.import_remote_profile_message, profile.name, profile.host)) - .setPositiveButton(android.R.string.ok) { _,_ -> - startActivity(Intent(this, NewProfileActivity::class.java).apply { - putExtra("importName", profile.name) - putExtra("importURL", profile.url) - }) + if (uri.scheme == "sing-box" && uri.host != "import-remote-profile") { + val profile = try { + Libbox.parseRemoteProfileImportLink(uri.toString()) + } catch (e: Exception) { + errorDialogBuilder(e).show() + return } - .setNegativeButton(android.R.string.cancel, null) - .show() + MaterialAlertDialogBuilder(this) + .setTitle(R.string.import_remote_profile) + .setMessage( + getString( + R.string.import_remote_profile_message, + profile.name, + profile.host + ) + ) + .setPositiveButton(android.R.string.ok) { _, _ -> + startActivity(Intent(this, NewProfileActivity::class.java).apply { + putExtra("importName", profile.name) + putExtra("importURL", profile.url) + }) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } else if (intent.action == Intent.ACTION_VIEW) { + try { + val data = contentResolver.openInputStream(uri)?.use { it.readBytes() } ?: return + val content = Libbox.decodeProfileContent(data) + MaterialAlertDialogBuilder(this) + .setTitle(R.string.import_profile) + .setMessage( + getString( + R.string.import_profile_message, + content.name + ) + ) + .setPositiveButton(android.R.string.ok) { _, _ -> + lifecycleScope.launch { + withContext(Dispatchers.IO) { + runCatching { + importProfile(content) + }.onFailure { + withContext(Dispatchers.Main) { + errorDialogBuilder(it).show() + } + } + } + } + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } catch (e: Exception) { + errorDialogBuilder(e).show() + } + } + } + + private suspend fun importProfile(content: ProfileContent) { + val typedProfile = TypedProfile() + val profile = Profile(name = content.name, typed = typedProfile) + profile.userOrder = ProfileManager.nextOrder() + when (content.type) { + Libbox.ProfileTypeLocal -> { + typedProfile.type = TypedProfile.Type.Local + } + + Libbox.ProfileTypeiCloud -> { + errorDialogBuilder(R.string.icloud_profile_unsupported).show() + return + } + + Libbox.ProfileTypeRemote -> { + typedProfile.type = TypedProfile.Type.Remote + typedProfile.remoteURL = content.remotePath + typedProfile.lastUpdated = Date(content.lastUpdated) + } + } + val configDirectory = File(filesDir, "configs").also { it.mkdirs() } + val configFile = File(configDirectory, "${profile.userOrder}.json") + configFile.writeText(content.config) + typedProfile.path = configFile.path + ProfileManager.create(profile) } fun reconnect() { diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/main/ConfigurationFragment.kt b/app/src/main/java/io/nekohasekai/sfa/ui/main/ConfigurationFragment.kt index 932be86..6e8434f 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ui/main/ConfigurationFragment.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ui/main/ConfigurationFragment.kt @@ -12,15 +12,14 @@ import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import io.nekohasekai.libbox.Libbox import io.nekohasekai.sfa.R import io.nekohasekai.sfa.database.Profile -import io.nekohasekai.sfa.database.Profiles +import io.nekohasekai.sfa.database.ProfileManager import io.nekohasekai.sfa.database.TypedProfile import io.nekohasekai.sfa.databinding.FragmentConfigurationBinding import io.nekohasekai.sfa.databinding.ViewConfigutationItemBinding import io.nekohasekai.sfa.ktx.errorDialogBuilder -import io.nekohasekai.sfa.ui.MainActivity +import io.nekohasekai.sfa.ktx.shareProfile import io.nekohasekai.sfa.ui.profile.EditProfileActivity import io.nekohasekai.sfa.ui.profile.NewProfileActivity import kotlinx.coroutines.CoroutineScope @@ -65,6 +64,7 @@ class ConfigurationFragment : Fragment() { binding.fab.setOnClickListener { startActivity(Intent(requireContext(), NewProfileActivity::class.java)) } + ProfileManager.registerCallback(this::updateProfiles) return binding.root } @@ -75,12 +75,17 @@ class ConfigurationFragment : Fragment() { override fun onDestroyView() { super.onDestroyView() + ProfileManager.unregisterCallback(this::updateProfiles) _adapter = null } + private fun updateProfiles() { + _adapter?.reload() + } + class Adapter( internal val scope: CoroutineScope, - private val parent: FragmentConfigurationBinding + internal val parent: FragmentConfigurationBinding ) : RecyclerView.Adapter() { @@ -88,7 +93,7 @@ class ConfigurationFragment : Fragment() { internal fun reload() { scope.launch(Dispatchers.IO) { - items = Profiles.list().toMutableList() + items = ProfileManager.list().toMutableList() withContext(Dispatchers.Main) { if (items.isEmpty()) { parent.statusText.isVisible = true @@ -120,7 +125,7 @@ class ConfigurationFragment : Fragment() { updated.add(first) notifyItemMoved(from, to) GlobalScope.launch(Dispatchers.IO) { - Profiles.update(updated) + ProfileManager.update(updated) } return true } @@ -166,18 +171,14 @@ class ConfigurationFragment : Fragment() { popup.setOnMenuItemClickListener { when (it.itemId) { R.id.action_share -> { - try { - val link = Libbox.generateRemoteProfileImportLink( - profile.name, - profile.typed.remoteURL - ) - button.context.startActivity(Intent.createChooser(Intent(android.content.Intent.ACTION_SEND).apply { - type = "text/plain" - putExtra(Intent.EXTRA_SUBJECT, "Share profile ${profile.name}") - putExtra(Intent.EXTRA_TEXT, link) - }, "Share")) - } catch (e: Exception) { - button.context.errorDialogBuilder(e).show() + adapter.scope.launch(Dispatchers.IO) { + try { + button.context.shareProfile(profile) + } catch (e: Exception) { + withContext(Dispatchers.Main) { + button.context.errorDialogBuilder(e).show() + } + } } true } @@ -187,7 +188,7 @@ class ConfigurationFragment : Fragment() { adapter.notifyItemRemoved(adapterPosition) adapter.scope.launch(Dispatchers.IO) { runCatching { - Profiles.delete(profile) + ProfileManager.delete(profile) } } true diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/main/DashboardFragment.kt b/app/src/main/java/io/nekohasekai/sfa/ui/main/DashboardFragment.kt index 8b7c60b..af26682 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ui/main/DashboardFragment.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ui/main/DashboardFragment.kt @@ -1,7 +1,6 @@ package io.nekohasekai.sfa.ui.main import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -22,7 +21,7 @@ import io.nekohasekai.sfa.R import io.nekohasekai.sfa.bg.BoxService import io.nekohasekai.sfa.constant.Status import io.nekohasekai.sfa.database.Profile -import io.nekohasekai.sfa.database.Profiles +import io.nekohasekai.sfa.database.ProfileManager import io.nekohasekai.sfa.database.Settings import io.nekohasekai.sfa.databinding.FragmentDashboardBinding import io.nekohasekai.sfa.databinding.ViewProfileItemBinding @@ -102,6 +101,8 @@ class DashboardFragment : Fragment(), CommandClientHandler { else -> {} } } + + ProfileManager.registerCallback(this::updateProfiles) } private fun reconnect() { @@ -139,6 +140,11 @@ class DashboardFragment : Fragment(), CommandClientHandler { _adapter = null _binding = null disconnect() + ProfileManager.unregisterCallback(this::updateProfiles) + } + + private fun updateProfiles() { + _adapter?.reload() } override fun connected() { @@ -192,7 +198,7 @@ class DashboardFragment : Fragment(), CommandClientHandler { internal var lastSelectedIndex: Int? = null internal fun reload() { scope.launch(Dispatchers.IO) { - items = Profiles.list().toMutableList() + items = ProfileManager.list().toMutableList() if (items.isNotEmpty()) { selectedProfileID = Settings.selectedProfile for ((index, profile) in items.withIndex()) { diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/main/SettingsFragment.kt b/app/src/main/java/io/nekohasekai/sfa/ui/main/SettingsFragment.kt index a797668..672d463 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ui/main/SettingsFragment.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ui/main/SettingsFragment.kt @@ -24,7 +24,7 @@ import io.nekohasekai.sfa.ktx.launchCustomTab import io.nekohasekai.sfa.ktx.setSimpleItems import io.nekohasekai.sfa.ktx.text import io.nekohasekai.sfa.ui.MainActivity -import io.nekohasekai.sfa.ui.configoverride.ConfigOverrideActivity +import io.nekohasekai.sfa.ui.profileoverride.ProfileOverrideActivity import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -104,7 +104,7 @@ class SettingsFragment : Fragment() { ) } binding.configureOverridesButton.setOnClickListener { - startActivity(Intent(requireContext(), ConfigOverrideActivity::class.java)) + startActivity(Intent(requireContext(), ProfileOverrideActivity::class.java)) } binding.communityButton.setOnClickListener { it.context.launchCustomTab("https://community.sagernet.org/") diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/profile/EditProfileActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/profile/EditProfileActivity.kt index ff6e99d..f7bb253 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ui/profile/EditProfileActivity.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ui/profile/EditProfileActivity.kt @@ -10,12 +10,13 @@ import io.nekohasekai.sfa.R import io.nekohasekai.sfa.bg.UpdateProfileWork import io.nekohasekai.sfa.constant.EnabledType import io.nekohasekai.sfa.database.Profile -import io.nekohasekai.sfa.database.Profiles +import io.nekohasekai.sfa.database.ProfileManager import io.nekohasekai.sfa.database.TypedProfile import io.nekohasekai.sfa.databinding.ActivityEditProfileBinding import io.nekohasekai.sfa.ktx.addTextChangedListener import io.nekohasekai.sfa.ktx.errorDialogBuilder import io.nekohasekai.sfa.ktx.setSimpleItems +import io.nekohasekai.sfa.ktx.shareProfile import io.nekohasekai.sfa.ktx.text import io.nekohasekai.sfa.ui.shared.AbstractActivity import io.nekohasekai.sfa.utils.HTTPClient @@ -57,14 +58,14 @@ class EditProfileActivity : AbstractActivity() { val profileId = intent.getLongExtra("profile_id", -1L) if (profileId == -1L) error("invalid arguments") - _profile = Profiles.get(profileId) ?: error("invalid arguments") + _profile = ProfileManager.get(profileId) ?: error("invalid arguments") withContext(Dispatchers.Main) { binding.name.text = profile.name binding.name.addTextChangedListener { lifecycleScope.launch(Dispatchers.IO) { try { profile.name = it - Profiles.update(profile) + ProfileManager.update(profile) } catch (e: Exception) { errorDialogBuilder(e).show() } @@ -103,6 +104,7 @@ class EditProfileActivity : AbstractActivity() { binding.autoUpdateInterval.addTextChangedListener(this@EditProfileActivity::updateAutoUpdateInterval) binding.updateButton.setOnClickListener(this@EditProfileActivity::updateProfile) binding.checkButton.setOnClickListener(this@EditProfileActivity::checkProfile) + binding.shareButton.setOnClickListener(this@EditProfileActivity::shareProfile) binding.profileLayout.isVisible = true binding.progressView.isVisible = false } @@ -155,7 +157,7 @@ class EditProfileActivity : AbstractActivity() { lifecycleScope.launch(Dispatchers.IO) { delay(200) try { - Profiles.update(profile) + ProfileManager.update(profile) } catch (e: Exception) { withContext(Dispatchers.Main) { errorDialogBuilder(e).show() @@ -175,7 +177,7 @@ class EditProfileActivity : AbstractActivity() { Libbox.checkConfig(content) File(profile.typed.path).writeText(content) profile.typed.lastUpdated = Date() - Profiles.update(profile) + ProfileManager.update(profile) } catch (e: Exception) { withContext(Dispatchers.Main) { errorDialogBuilder(e).show() @@ -206,4 +208,16 @@ class EditProfileActivity : AbstractActivity() { } } + private fun shareProfile(button: View) { + lifecycleScope.launch(Dispatchers.IO) { + try { + shareProfile(profile) + } catch (e: Exception) { + withContext(Dispatchers.Main) { + errorDialogBuilder(e).show() + } + } + } + } + } \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/profile/EditProfileContentActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/profile/EditProfileContentActivity.kt index 7131c17..6df964f 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ui/profile/EditProfileContentActivity.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ui/profile/EditProfileContentActivity.kt @@ -11,7 +11,7 @@ import com.blacksquircle.ui.language.json.JsonLanguage import io.nekohasekai.libbox.Libbox import io.nekohasekai.sfa.R import io.nekohasekai.sfa.database.Profile -import io.nekohasekai.sfa.database.Profiles +import io.nekohasekai.sfa.database.ProfileManager import io.nekohasekai.sfa.databinding.ActivityEditProfileContentBinding import io.nekohasekai.sfa.ktx.errorDialogBuilder import io.nekohasekai.sfa.ui.shared.AbstractActivity @@ -114,7 +114,7 @@ class EditProfileContentActivity : AbstractActivity() { val profileId = intent.getLongExtra("profile_id", -1L) if (profileId == -1L) error("invalid arguments") - _profile = Profiles.get(profileId) ?: error("invalid arguments") + _profile = ProfileManager.get(profileId) ?: error("invalid arguments") val content = File(profile.typed.path).readText() withContext(Dispatchers.Main) { binding.editor.setTextContent(content) diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/profile/NewProfileActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/profile/NewProfileActivity.kt index 40ab654..2e0fc70 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ui/profile/NewProfileActivity.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ui/profile/NewProfileActivity.kt @@ -9,7 +9,7 @@ import androidx.lifecycle.lifecycleScope import io.nekohasekai.libbox.Libbox import io.nekohasekai.sfa.R import io.nekohasekai.sfa.database.Profile -import io.nekohasekai.sfa.database.Profiles +import io.nekohasekai.sfa.database.ProfileManager import io.nekohasekai.sfa.database.TypedProfile import io.nekohasekai.sfa.databinding.ActivityAddProfileBinding import io.nekohasekai.sfa.ktx.addTextChangedListener @@ -83,7 +83,7 @@ class NewProfileActivity : AbstractActivity() { } binding.createProfile.setOnClickListener(this::createProfile) intent.getStringExtra("importName")?.also { importName -> - intent.getStringExtra("importURL") ?.also { importURL -> + intent.getStringExtra("importURL")?.also { importURL -> binding.name.editText?.setText(importName) binding.type.text = TypedProfile.Type.Remote.name binding.remoteURL.editText?.setText(importURL) @@ -128,7 +128,7 @@ class NewProfileActivity : AbstractActivity() { private suspend fun createProfile0() { val typedProfile = TypedProfile() val profile = Profile(name = binding.name.text, typed = typedProfile) - profile.userOrder = Profiles.nextOrder() + profile.userOrder = ProfileManager.nextOrder() when (binding.type.text) { TypedProfile.Type.Local.name -> { @@ -174,7 +174,7 @@ class NewProfileActivity : AbstractActivity() { typedProfile.lastUpdated = Date() } } - Profiles.create(profile) + ProfileManager.create(profile) withContext(Dispatchers.Main) { binding.progressView.isVisible = false finish() diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/configoverride/PerAppProxyActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/PerAppProxyActivity.kt similarity index 99% rename from app/src/main/java/io/nekohasekai/sfa/ui/configoverride/PerAppProxyActivity.kt rename to app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/PerAppProxyActivity.kt index 941b3ac..24b5b27 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ui/configoverride/PerAppProxyActivity.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/PerAppProxyActivity.kt @@ -1,4 +1,4 @@ -package io.nekohasekai.sfa.ui.configoverride +package io.nekohasekai.sfa.ui.profileoverride import android.Manifest import android.annotation.SuppressLint @@ -28,7 +28,6 @@ import io.nekohasekai.sfa.R import io.nekohasekai.sfa.database.Settings import io.nekohasekai.sfa.databinding.ActivityPerAppProxyBinding import io.nekohasekai.sfa.databinding.ViewAppListItemBinding -import io.nekohasekai.sfa.ktx.errorDialogBuilder import io.nekohasekai.sfa.ui.shared.AbstractActivity import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -242,7 +241,7 @@ class PerAppProxyActivity : AbstractActivity() { appNameMap[it.packageName] = it.name } val foundChinaApps = mutableMapOf() - scanChinaApps(appList.map { it.packageName }).forEach {packageName -> + scanChinaApps(appList.map { it.packageName }).forEach { packageName -> foundChinaApps[packageName] = appNameMap[packageName] ?: "Unknown" } foundChinaApps diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/configoverride/ConfigOverrideActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/ProfileOverrideActivity.kt similarity index 89% rename from app/src/main/java/io/nekohasekai/sfa/ui/configoverride/ConfigOverrideActivity.kt rename to app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/ProfileOverrideActivity.kt index f41b39f..0ada5fc 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ui/configoverride/ConfigOverrideActivity.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/ProfileOverrideActivity.kt @@ -1,4 +1,4 @@ -package io.nekohasekai.sfa.ui.configoverride +package io.nekohasekai.sfa.ui.profileoverride import android.content.Intent import android.os.Bundle @@ -15,13 +15,13 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class ConfigOverrideActivity : AbstractActivity() { +class ProfileOverrideActivity : AbstractActivity() { private lateinit var binding: ActivityConfigOverrideBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setTitle(R.string.title_config_override) + setTitle(R.string.title_profile_override) binding = ActivityConfigOverrideBinding.inflate(layoutInflater) setContentView(binding.root) supportActionBar?.setDisplayHomeAsUpEnabled(true) @@ -52,7 +52,8 @@ class ConfigOverrideActivity : AbstractActivity() { private suspend fun reloadSettings() { val perAppUpdateOnChange = Settings.perAppProxyUpdateOnChange withContext(Dispatchers.Main) { - binding.perAppProxyUpdateOnChange.text = PerAppProxyUpdateType.valueOf(perAppUpdateOnChange).name + binding.perAppProxyUpdateOnChange.text = + PerAppProxyUpdateType.valueOf(perAppUpdateOnChange).name binding.perAppProxyUpdateOnChange.setSimpleItems(R.array.per_app_proxy_update_on_change_value) } } diff --git a/app/src/main/res/layout/activity_config_override.xml b/app/src/main/res/layout/activity_config_override.xml index f5e1b52..c2803fa 100644 --- a/app/src/main/res/layout/activity_config_override.xml +++ b/app/src/main/res/layout/activity_config_override.xml @@ -78,7 +78,7 @@ style="@style/Widget.Material3.Button.TextButton" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/config_override_configure" /> + android:text="@string/profile_override_configure" /> diff --git a/app/src/main/res/layout/activity_edit_profile.xml b/app/src/main/res/layout/activity_edit_profile.xml index 125f2b8..7005a82 100644 --- a/app/src/main/res/layout/activity_edit_profile.xml +++ b/app/src/main/res/layout/activity_edit_profile.xml @@ -135,6 +135,14 @@ +