Add support for import remote profile

This commit is contained in:
世界 2023-07-27 12:14:03 +08:00
parent 805d99e297
commit cb9799936b
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
8 changed files with 92 additions and 7 deletions

View file

@ -39,6 +39,19 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter android:label="@string/import_remote_profile">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="import-remote-profile"
android:scheme="sing-box" />
</intent-filter>
<meta-data <meta-data
android:name="android.app.shortcuts" android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" /> android:resource="@xml/shortcuts" />

View file

@ -9,14 +9,14 @@ fun Context.errorDialogBuilder(@StringRes messageId: Int): MaterialAlertDialogBu
return MaterialAlertDialogBuilder(this) return MaterialAlertDialogBuilder(this)
.setTitle(R.string.error_title) .setTitle(R.string.error_title)
.setMessage(messageId) .setMessage(messageId)
.setPositiveButton(resources.getString(android.R.string.ok), null) .setPositiveButton(android.R.string.ok, null)
} }
fun Context.errorDialogBuilder(message: String): MaterialAlertDialogBuilder { fun Context.errorDialogBuilder(message: String): MaterialAlertDialogBuilder {
return MaterialAlertDialogBuilder(this) return MaterialAlertDialogBuilder(this)
.setTitle(R.string.error_title) .setTitle(R.string.error_title)
.setMessage(message) .setMessage(message)
.setPositiveButton(resources.getString(android.R.string.ok), null) .setPositiveButton(android.R.string.ok, null)
} }
fun Context.errorDialogBuilder(exception: Throwable): MaterialAlertDialogBuilder { fun Context.errorDialogBuilder(exception: Throwable): MaterialAlertDialogBuilder {

View file

@ -26,6 +26,7 @@ import com.microsoft.appcenter.distribute.DistributeListener
import com.microsoft.appcenter.distribute.ReleaseDetails import com.microsoft.appcenter.distribute.ReleaseDetails
import com.microsoft.appcenter.distribute.UpdateAction import com.microsoft.appcenter.distribute.UpdateAction
import com.microsoft.appcenter.utils.AppNameHelper import com.microsoft.appcenter.utils.AppNameHelper
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.sfa.Application import io.nekohasekai.sfa.Application
import io.nekohasekai.sfa.BuildConfig import io.nekohasekai.sfa.BuildConfig
import io.nekohasekai.sfa.R 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.database.Settings
import io.nekohasekai.sfa.databinding.ActivityMainBinding import io.nekohasekai.sfa.databinding.ActivityMainBinding
import io.nekohasekai.sfa.ktx.errorDialogBuilder import io.nekohasekai.sfa.ktx.errorDialogBuilder
import io.nekohasekai.sfa.ui.profile.NewProfileActivity
import io.nekohasekai.sfa.ui.shared.AbstractActivity import io.nekohasekai.sfa.ui.shared.AbstractActivity
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -80,6 +82,31 @@ class MainActivity : AbstractActivity(), ServiceConnection.Callback, DistributeL
startAnalysis() 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() { fun reconnect() {
connection.reconnect() connection.reconnect()
} }
@ -104,7 +131,7 @@ class MainActivity : AbstractActivity(), ServiceConnection.Callback, DistributeL
val builder = MaterialAlertDialogBuilder(this) val builder = MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.analytics_title)) .setTitle(getString(R.string.analytics_title))
.setMessage(getString(R.string.analytics_message)) .setMessage(getString(R.string.analytics_message))
.setPositiveButton(getString(R.string.ok)) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
Settings.analyticsAllowed = Settings.ANALYSIS_ALLOWED Settings.analyticsAllowed = Settings.ANALYSIS_ALLOWED
startAnalysisInternal() startAnalysisInternal()
@ -258,7 +285,7 @@ class MainActivity : AbstractActivity(), ServiceConnection.Callback, DistributeL
override fun onServiceAlert(type: Alert, message: String?) { override fun onServiceAlert(type: Alert, message: String?) {
val builder = MaterialAlertDialogBuilder(this) val builder = MaterialAlertDialogBuilder(this)
builder.setPositiveButton(resources.getString(android.R.string.ok), null) builder.setPositiveButton(android.R.string.ok, null)
when (type) { when (type) {
Alert.RequestVPNPermission -> { Alert.RequestVPNPermission -> {
builder.setMessage(getString(R.string.service_error_missing_permission)) builder.setMessage(getString(R.string.service_error_missing_permission))

View file

@ -12,11 +12,15 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.sfa.R import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.database.Profile import io.nekohasekai.sfa.database.Profile
import io.nekohasekai.sfa.database.Profiles import io.nekohasekai.sfa.database.Profiles
import io.nekohasekai.sfa.database.TypedProfile
import io.nekohasekai.sfa.databinding.FragmentConfigurationBinding import io.nekohasekai.sfa.databinding.FragmentConfigurationBinding
import io.nekohasekai.sfa.databinding.ViewConfigutationItemBinding 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.EditProfileActivity
import io.nekohasekai.sfa.ui.profile.NewProfileActivity import io.nekohasekai.sfa.ui.profile.NewProfileActivity
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -152,12 +156,32 @@ class ConfigurationFragment : Fragment() {
intent.putExtra("profile_id", profile.id) intent.putExtra("profile_id", profile.id)
it.context.startActivity(intent) it.context.startActivity(intent)
} }
binding.moreButton.setOnClickListener { it -> binding.moreButton.setOnClickListener { button ->
val popup = PopupMenu(it.context, it) val popup = PopupMenu(button.context, button)
popup.setForceShowIcon(true) popup.setForceShowIcon(true)
popup.menuInflater.inflate(R.menu.profile_menu, popup.menu) 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 { popup.setOnMenuItemClickListener {
when (it.itemId) { 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 -> { R.id.action_delete -> {
adapter.items.remove(profile) adapter.items.remove(profile)
adapter.notifyItemRemoved(adapterPosition) adapter.notifyItemRemoved(adapterPosition)

View file

@ -82,6 +82,13 @@ class NewProfileActivity : AbstractActivity() {
startFilesForResult(importFile, "application/json") startFilesForResult(importFile, "application/json")
} }
binding.createProfile.setOnClickListener(this::createProfile) 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) { private fun createProfile(view: View) {

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M16,5l-1.42,1.42 -1.59,-1.59L12.99,16h-1.98L11.01,4.83L9.42,6.42 8,5l4,-4 4,4zM20,10v11c0,1.1 -0.9,2 -2,2L6,23c-1.11,0 -2,-0.9 -2,-2L4,10c0,-1.11 0.89,-2 2,-2h3v2L6,10v11h12L18,10h-3L15,8h3c1.1,0 2,0.89 2,2z"/>
</vector>

View file

@ -2,6 +2,13 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_share"
android:title="@string/menu_share"
android:icon="@drawable/ic_ios_share_24"
app:iconTintMode="src_in"
app:iconTint="?colorPrimary" />
<item <item
android:id="@+id/action_delete" android:id="@+id/action_delete"
android:title="@string/menu_delete" android:title="@string/menu_delete"

View file

@ -37,6 +37,7 @@
<string name="menu_redo">Redo</string> <string name="menu_redo">Redo</string>
<string name="menu_format">Format</string> <string name="menu_format">Format</string>
<string name="menu_delete">Delete</string> <string name="menu_delete">Delete</string>
<string name="menu_share">Share</string>
<string name="status_default">Service not started</string> <string name="status_default">Service not started</string>
<string name="status_starting">Service starting…</string> <string name="status_starting">Service starting…</string>
@ -75,7 +76,6 @@
<string name="analytics_title">Analytics</string> <string name="analytics_title">Analytics</string>
<string name="analytics_message">Would you like to give SFA permission to collect analytics, send crash reports, and check update through AppCenter?</string> <string name="analytics_message">Would you like to give SFA permission to collect analytics, send crash reports, and check update through AppCenter?</string>
<string name="no_thanks">No, thanks</string> <string name="no_thanks">No, thanks</string>
<string name="ok">Ok</string>
<string name="check_update">Check Update</string> <string name="check_update">Check Update</string>
<string name="title_app_center">App Center</string> <string name="title_app_center">App Center</string>
<string name="title_feedback">Feedback</string> <string name="title_feedback">Feedback</string>
@ -91,5 +91,7 @@
<string name="background_permission_description">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.</string> <string name="background_permission_description">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.</string>
<string name="read_more">Read More</string> <string name="read_more">Read More</string>
<string name="request_background_permission">Ignore battery optimizations</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>
</resources> </resources>