diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 19cba10..483cc14 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -39,6 +39,19 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/io/nekohasekai/sfa/ktx/Dialogs.kt b/app/src/main/java/io/nekohasekai/sfa/ktx/Dialogs.kt
index 02a3bc1..80a9866 100644
--- a/app/src/main/java/io/nekohasekai/sfa/ktx/Dialogs.kt
+++ b/app/src/main/java/io/nekohasekai/sfa/ktx/Dialogs.kt
@@ -9,14 +9,14 @@ fun Context.errorDialogBuilder(@StringRes messageId: Int): MaterialAlertDialogBu
return MaterialAlertDialogBuilder(this)
.setTitle(R.string.error_title)
.setMessage(messageId)
- .setPositiveButton(resources.getString(android.R.string.ok), null)
+ .setPositiveButton(android.R.string.ok, null)
}
fun Context.errorDialogBuilder(message: String): MaterialAlertDialogBuilder {
return MaterialAlertDialogBuilder(this)
.setTitle(R.string.error_title)
.setMessage(message)
- .setPositiveButton(resources.getString(android.R.string.ok), null)
+ .setPositiveButton(android.R.string.ok, null)
}
fun Context.errorDialogBuilder(exception: Throwable): MaterialAlertDialogBuilder {
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 64fd11a..7a63be8 100644
--- a/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt
+++ b/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt
@@ -26,6 +26,7 @@ import com.microsoft.appcenter.distribute.DistributeListener
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.sfa.Application
import io.nekohasekai.sfa.BuildConfig
import io.nekohasekai.sfa.R
@@ -37,6 +38,7 @@ import io.nekohasekai.sfa.constant.Status
import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.databinding.ActivityMainBinding
import io.nekohasekai.sfa.ktx.errorDialogBuilder
+import io.nekohasekai.sfa.ui.profile.NewProfileActivity
import io.nekohasekai.sfa.ui.shared.AbstractActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@@ -80,6 +82,31 @@ class MainActivity : AbstractActivity(), ServiceConnection.Callback, DistributeL
startAnalysis()
}
+ 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)
+ })
+ }
+ .setNegativeButton(android.R.string.cancel, null)
+ .show()
+ }
+
fun reconnect() {
connection.reconnect()
}
@@ -104,7 +131,7 @@ class MainActivity : AbstractActivity(), ServiceConnection.Callback, DistributeL
val builder = MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.analytics_title))
.setMessage(getString(R.string.analytics_message))
- .setPositiveButton(getString(R.string.ok)) { _, _ ->
+ .setPositiveButton(android.R.string.ok) { _, _ ->
lifecycleScope.launch(Dispatchers.IO) {
Settings.analyticsAllowed = Settings.ANALYSIS_ALLOWED
startAnalysisInternal()
@@ -258,7 +285,7 @@ class MainActivity : AbstractActivity(), ServiceConnection.Callback, DistributeL
override fun onServiceAlert(type: Alert, message: String?) {
val builder = MaterialAlertDialogBuilder(this)
- builder.setPositiveButton(resources.getString(android.R.string.ok), null)
+ builder.setPositiveButton(android.R.string.ok, null)
when (type) {
Alert.RequestVPNPermission -> {
builder.setMessage(getString(R.string.service_error_missing_permission))
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 94c02d7..932be86 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,11 +12,15 @@ 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.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.ui.profile.EditProfileActivity
import io.nekohasekai.sfa.ui.profile.NewProfileActivity
import kotlinx.coroutines.CoroutineScope
@@ -152,12 +156,32 @@ class ConfigurationFragment : Fragment() {
intent.putExtra("profile_id", profile.id)
it.context.startActivity(intent)
}
- binding.moreButton.setOnClickListener { it ->
- val popup = PopupMenu(it.context, it)
+ binding.moreButton.setOnClickListener { button ->
+ val popup = PopupMenu(button.context, button)
popup.setForceShowIcon(true)
popup.menuInflater.inflate(R.menu.profile_menu, popup.menu)
+ if (profile.typed.type != TypedProfile.Type.Remote) {
+ popup.menu.removeItem(R.id.action_share)
+ }
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()
+ }
+ true
+ }
+
R.id.action_delete -> {
adapter.items.remove(profile)
adapter.notifyItemRemoved(adapterPosition)
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 5dd5914..40ab654 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
@@ -82,6 +82,13 @@ class NewProfileActivity : AbstractActivity() {
startFilesForResult(importFile, "application/json")
}
binding.createProfile.setOnClickListener(this::createProfile)
+ intent.getStringExtra("importName")?.also { importName ->
+ intent.getStringExtra("importURL") ?.also { importURL ->
+ binding.name.editText?.setText(importName)
+ binding.type.text = TypedProfile.Type.Remote.name
+ binding.remoteURL.editText?.setText(importURL)
+ }
+ }
}
private fun createProfile(view: View) {
diff --git a/app/src/main/res/drawable/ic_ios_share_24.xml b/app/src/main/res/drawable/ic_ios_share_24.xml
new file mode 100644
index 0000000..d426516
--- /dev/null
+++ b/app/src/main/res/drawable/ic_ios_share_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/menu/profile_menu.xml b/app/src/main/res/menu/profile_menu.xml
index 815640a..406f7a5 100644
--- a/app/src/main/res/menu/profile_menu.xml
+++ b/app/src/main/res/menu/profile_menu.xml
@@ -2,6 +2,13 @@