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 @@ + + Redo Format Delete + Share Service not started Service starting… @@ -75,7 +76,6 @@ Analytics Would you like to give SFA permission to collect analytics, send crash reports, and check update through AppCenter? No, thanks - Ok Check Update App Center Feedback @@ -91,5 +91,7 @@ Apply for the necessary permissions in order for the VPN to function properly.\n\nIf you are using a device made by a Chinese company, the card may not disappear after the permission is granted. Read More Ignore battery optimizations + Import remote profile + Are you sure to import remote configuration %s? You will connect to %s to download the configuration. \ No newline at end of file