Add profile sharing

This commit is contained in:
世界 2023-07-30 21:16:05 +08:00
parent 9575764f40
commit fe0b3fdce3
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
22 changed files with 302 additions and 87 deletions

View file

@ -40,7 +40,7 @@
</intent-filter>
<intent-filter android:label="@string/import_remote_profile">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
@ -52,6 +52,23 @@
</intent-filter>
<intent-filter android:priority="999">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.OPENABLE" />
<data android:host="*" />
<data android:mimeType="application/octet-stream" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.bpf" />
<data android:pathPattern=".*\\..*\\..*\\..*\\.bpf" />
<data android:pathPattern=".*\\..*\\..*\\.bpf" />
<data android:pathPattern=".*\\..*\\.bpf" />
<data android:pathPattern=".*\\.bpf" />
<data android:scheme="content" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
@ -79,10 +96,10 @@
android:name="io.nekohasekai.sfa.ui.profile.EditProfileContentActivity"
android:exported="false" />
<activity
android:name="io.nekohasekai.sfa.ui.configoverride.ConfigOverrideActivity"
android:name="io.nekohasekai.sfa.ui.profileoverride.ProfileOverrideActivity"
android:exported="false" />
<activity
android:name="io.nekohasekai.sfa.ui.configoverride.PerAppProxyActivity"
android:name="io.nekohasekai.sfa.ui.profileoverride.PerAppProxyActivity"
android:exported="false" />
<service
@ -125,6 +142,16 @@
android:name="io.nekohasekai.sfa.bg.AppChangeReceiver"
android:exported="true" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.cache"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/cache_paths" />
</provider>
</application>
</manifest>

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Profile>): 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<Profile>): Int {
return instance.profileDao().delete(profiles)
try {
return instance.profileDao().delete(profiles)
} finally {
for (callback in callbacks.toList()) {
callback()
}
}
}
suspend fun list(): List<Profile> {

View file

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

View file

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

View file

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

View file

@ -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<Holder>() {
@ -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

View file

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

View file

@ -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/")

View file

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

View file

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

View file

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

View file

@ -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<String, String>()
scanChinaApps(appList.map { it.packageName }).forEach {packageName ->
scanChinaApps(appList.map { it.packageName }).forEach { packageName ->
foundChinaApps[packageName] = appNameMap[packageName] ?: "Unknown"
}
foundChinaApps

View file

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

View file

@ -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" />
</LinearLayout>
</LinearLayout>

View file

@ -135,6 +135,14 @@
</LinearLayout>
<Button
android:id="@+id/shareButton"
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/profile_share" />
<Button
android:id="@+id/checkButton"
style="@style/Widget.Material3.Button.ElevatedButton"

View file

@ -251,7 +251,7 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_config_override"
android:text="@string/title_profile_override"
android:textAppearance="?attr/textAppearanceTitleLarge">
</TextView>
@ -260,7 +260,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/config_override_description" />
android:text="@string/profile_override_description" />
<LinearLayout
android:layout_width="match_parent"
@ -273,7 +273,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" />
</LinearLayout>

View file

@ -20,6 +20,7 @@
<string name="profile_create">Create</string>
<string name="profile_edit_content">Edit Content</string>
<string name="profile_check">Check</string>
<string name="profile_share">Share</string>
<string name="profile_input_required">Required</string>
<string name="profile_empty">Empty profiles</string>
<string name="profile_last_updated">Last Updated</string>
@ -92,11 +93,11 @@
<string name="read_more">Read More</string>
<string name="request_background_permission">Ignore battery optimizations</string>
<string name="import_remote_profile">Import remote profile</string>
<string name="import_remote_profile_message">Are you sure to import remote configuration %s? You will connect to %s to download the configuration.</string>
<string name="import_remote_profile_message">Are you sure to import remote profile %s? You will connect to %s to download the configuration.</string>
<string name="title_config_override">Config Override</string>
<string name="config_override_description">Override configuration contents.</string>
<string name="config_override_configure">Configure</string>
<string name="title_profile_override">Profile Override</string>
<string name="profile_override_description">Overrides profile configuration items with platform-specific values.</string>
<string name="profile_override_configure">Configure</string>
<string name="title_per_app_proxy">Per-app Proxy</string>
<string name="per_app_proxy_description">Override include_package and exclude_package in the configuration.</string>
<string name="per_app_proxy_mode_exclude">Do not proxy selected apps</string>
@ -120,4 +121,7 @@
<string name="action_select">Select</string>
<string name="action_deselect">Deselect</string>
<string name="per_app_proxy_update_on_change">Update on App Installed/Updated</string>
<string name="import_profile">Import profile</string>
<string name="import_profile_message">Are you sure to import profile %s?</string>
<string name="icloud_profile_unsupported">iCloud profile is not support on current platform</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="cache" path="/"/>
</paths>