世界 4 anni fa
parent
commit
05db505692

+ 3 - 0
.idea/misc.xml

@@ -1,5 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
+  <component name="ProjectPlainTextFileTypeManager">
+    <file url="file://$PROJECT_DIR$/README.md" />
+  </component>
   <component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
     <output url="file://$PROJECT_DIR$/build/classes" />
   </component>

+ 5 - 4
README.md

@@ -22,7 +22,6 @@ The application is designed to be used whenever possible.
 * Trojan
 * VLESS / Trojan + XTLS ( xtls-plugin )
 * Trojan-Go ( trojan-go-plugin )
-* Proxy Chain
 
 ### Subscription protocols
 
@@ -32,16 +31,18 @@ The application is designed to be used whenever possible.
 
 ## FEATURES
 
-* Full basic features  
-* Option to change the notification update interval  
+* Full basic features
+* V2Ray WebSocket Browser Forwarding
+* Option to change the notification update interval
 * A Chinese apps scanner (based on dex classpath scanning, so it may be slower)
+* Proxy Chain
+* Advanced routing with outbound profile selection support
 
 ## TIPS
 
 * Click on the title to scroll to the first proxy or the selected proxy  
 * Proxy list can be dragged by holding the progress bar  
 * The Chinese apps scanner will only scan system apps if "Show system apps" is checked  
-* ~~V2Ray browser forwarding is not yet available, only for developers to debug~~ Fixed
 
 ## OPEN SOURCE LICENSES
 

+ 2 - 2
app/build.gradle

@@ -6,8 +6,8 @@ plugins {
     id "com.mikepenz.aboutlibraries.plugin"
 }
 
-def verCode = 25
-def verName = "0.1-beta24"
+def verCode = 26
+def verName = "0.1-beta25"
 
 def keystorePwd = null
 def alias = null

+ 1 - 1
app/src/main/java/io/nekohasekai/sagernet/database/ProfileManager.kt

@@ -191,7 +191,7 @@ object ProfileManager {
         }
     }
 
-    suspend fun clear(groupId: Long) {
+    suspend fun clearGroup(groupId: Long) {
         DataStore.selectedProxy = 0L
         SagerDatabase.proxyDao.deleteAll(groupId)
         if (DataStore.directBootAware) DirectBoot.clean()

+ 2 - 2
app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt

@@ -178,7 +178,7 @@ data class ProxyEntity(
         }
     }
 
-    fun toUri(): String {
+    fun toUri(): String? {
         return when (type) {
             0 -> requireSOCKS().toUri()
             1 -> requireSS().toUri()
@@ -188,7 +188,7 @@ data class ProxyEntity(
             5 -> requireTrojan().toUri()
             6 -> requireHttp().toUri()
             7 -> requireTrojanGo().toUri()
-            else -> error("Undefined type $type")
+            else -> null
         }
     }
 

+ 3 - 0
app/src/main/java/io/nekohasekai/sagernet/database/RuleEntity.kt

@@ -104,6 +104,9 @@ data class RuleEntity(
         @Update
         fun updateRules(rules: List<RuleEntity>)
 
+        @Query("DELETE FROM rules")
+        fun deleteAll()
+
     }
 
 

+ 2 - 2
app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt

@@ -73,7 +73,7 @@ fun buildV2RayConfig(proxy: ProxyEntity): V2rayBuildResult {
     val proxies = proxy.resolveChain().asReversed()
     val extraRules = SagerDatabase.rulesDao.enabledRules()
     val extraProxies = SagerDatabase.proxyDao.getEntities(extraRules.mapNotNull { rule ->
-        rule.outbound.takeIf { it > 0 }
+        rule.outbound.takeIf { it > 0 && it != proxy.id }
     }.toHashSet().toList()).map { it.id to it.resolveChain() }.toMap()
 
     val bind = if (DataStore.allowAccess) "0.0.0.0" else "127.0.0.1"
@@ -660,7 +660,7 @@ fun buildV2RayConfig(proxy: ProxyEntity): V2rayBuildResult {
                     0L -> TAG_AGENT
                     -1L -> TAG_DIRECT
                     -2L -> TAG_BLOCK
-                    else -> "$TAG_AGENT-$outId"
+                    else -> if (outId == proxy.id) TAG_AGENT else "$TAG_AGENT-$outId"
                 }
             })
         }

+ 63 - 9
app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt

@@ -21,6 +21,8 @@
 
 package io.nekohasekai.sagernet.ui
 
+import android.content.ActivityNotFoundException
+import android.content.Context
 import android.content.Intent
 import android.graphics.Color
 import android.os.Bundle
@@ -32,6 +34,7 @@ import android.view.MenuItem
 import android.view.View
 import android.view.ViewGroup
 import android.widget.*
+import androidx.activity.result.ActivityResultLauncher
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.appcompat.app.AlertDialog
 import androidx.appcompat.widget.PopupMenu
@@ -156,8 +159,6 @@ class ConfigurationFragment @JvmOverloads constructor(
         super.onDestroy()
     }
 
-    fun snackbar(text: String) = (activity as MainActivity).snackbar(text)
-
     val importFile = registerForActivityResult(ActivityResultContracts.GetContent()) {
         runOnDefaultDispatcher {
             try {
@@ -253,10 +254,67 @@ class ConfigurationFragment @JvmOverloads constructor(
             R.id.action_new_chain -> {
                 startActivity(Intent(requireActivity(), ChainSettingsActivity::class.java))
             }
+            R.id.action_export_clipboard -> {
+                runOnDefaultDispatcher {
+                    val profiles = SagerDatabase.proxyDao.getByGroup(DataStore.selectedGroup)
+                    val links = profiles.mapNotNull { it.toUri() }.joinToString("\n")
+                    SagerNet.trySetPrimaryClip(links)
+                    onMainDispatcher {
+                        snackbar(getString(R.string.copy_toast_msg)).show()
+                    }
+                }
+            }
+            R.id.action_export_file -> {
+                startFilesForResult(exportProfiles)
+            }
+            R.id.action_clear -> {
+                runOnDefaultDispatcher {
+                    ProfileManager.clearGroup(DataStore.selectedGroup)
+                }
+            }
         }
         return true
     }
 
+    private fun startFilesForResult(launcher: ActivityResultLauncher<String>) {
+        try {
+            return launcher.launch("")
+        } catch (_: ActivityNotFoundException) {
+        } catch (_: SecurityException) {
+        }
+        (activity as MainActivity).snackbar(getString(R.string.file_manager_missing)).show()
+    }
+
+    class SaveProfiles : ActivityResultContracts.CreateDocument() {
+        override fun createIntent(context: Context, input: String) =
+            super.createIntent(context, "profiles.txt").apply { type = "text/plain" }
+    }
+
+
+    private val exportProfiles = registerForActivityResult(SaveProfiles()) { data ->
+        if (data != null) {
+            runOnDefaultDispatcher {
+                val profiles = SagerDatabase.proxyDao.getByGroup(DataStore.selectedGroup)
+                val links = profiles.mapNotNull { it.toUri() }.joinToString("\n")
+                try {
+                    (requireActivity() as MainActivity).contentResolver.openOutputStream(data)!!
+                        .bufferedWriter().use {
+                        it.write(links)
+                    }
+                    onMainDispatcher {
+                        snackbar(getString(R.string.copy_toast_msg)).show()
+                    }
+                } catch (e: Exception) {
+                    Logs.w(e)
+                    onMainDispatcher {
+                        snackbar(e.readableMessage).show()
+                    }
+                }
+
+            }
+        }
+    }
+
     inner class GroupPagerAdapter : FragmentStateAdapter(this), ProfileManager.GroupListener {
 
         var selectedGroupIndex = 0
@@ -645,11 +703,7 @@ class ConfigurationFragment @JvmOverloads constructor(
 
             override suspend fun onCleared(groupId: Long) {
                 if (groupId != proxyGroup.id) return
-                configurationListView.post {
-                    configurationList.clear()
-                    configurationList.clear()
-                    notifyDataSetChanged()
-                }
+                reloadProfiles(groupId)
             }
 
             override suspend fun reloadProfiles(groupId: Long) {
@@ -942,10 +996,10 @@ class ConfigurationFragment @JvmOverloads constructor(
                     when (item.itemId) {
                         // socks
                         R.id.action_qr_code -> {
-                            showCode(entity.toUri())
+                            showCode(entity.toUri()!!)
                         }
                         R.id.action_export_clipboard -> {
-                            export(entity.toUri())
+                            export(entity.toUri()!!)
                         }
                     }
                 } catch (e: Exception) {

+ 10 - 2
app/src/main/java/io/nekohasekai/sagernet/ui/RouteFragment.kt

@@ -36,6 +36,7 @@ import androidx.core.view.ViewCompat
 import androidx.recyclerview.widget.ItemTouchHelper
 import androidx.recyclerview.widget.RecyclerView
 import io.nekohasekai.sagernet.R
+import io.nekohasekai.sagernet.database.DataStore
 import io.nekohasekai.sagernet.database.ProfileManager
 import io.nekohasekai.sagernet.database.RuleEntity
 import io.nekohasekai.sagernet.database.SagerDatabase
@@ -126,6 +127,13 @@ class RouteFragment : ToolbarFragment(R.layout.layout_route), Toolbar.OnMenuItem
             R.id.action_new_route -> {
                 startActivity(Intent(context, RouteSettingsActivity::class.java))
             }
+            R.id.action_reset_route -> {
+                runOnDefaultDispatcher {
+                    SagerDatabase.rulesDao.deleteAll()
+                    DataStore.rulesFirstCreate = false
+                    ruleAdapter.reload()
+                }
+            }
         }
         return true
     }
@@ -137,9 +145,9 @@ class RouteFragment : ToolbarFragment(R.layout.layout_route), Toolbar.OnMenuItem
         val ruleList = ArrayList<RuleEntity>()
         suspend fun reload() {
             val rules = ProfileManager.getRules()
-            ruleList.clear()
-            ruleList.addAll(rules)
             ruleListView.post {
+                ruleList.clear()
+                ruleList.addAll(rules)
                 ruleAdapter.notifyDataSetChanged()
             }
         }

+ 3 - 4
app/src/main/java/io/nekohasekai/sagernet/ui/profile/ProfileSettingsActivity.kt

@@ -134,10 +134,6 @@ abstract class ProfileSettingsActivity<T : AbstractBean>(
                                 activity = this@ProfileSettingsActivity
                             })
                         .commit()
-
-                    DataStore.dirty = false
-
-                    DataStore.profileCacheStore.registerChangeListener(this@ProfileSettingsActivity)
                 }
             }
 
@@ -229,6 +225,9 @@ abstract class ProfileSettingsActivity<T : AbstractBean>(
             activity.apply {
                 viewCreated(view, savedInstanceState)
             }
+
+            DataStore.dirty = false
+            DataStore.profileCacheStore.registerChangeListener(activity)
         }
 
         override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {

+ 17 - 0
app/src/main/res/menu/add_profile_menu.xml

@@ -49,4 +49,21 @@
             </item>
         </menu>
     </item>
+    <item
+        android:icon="@drawable/ic_file_file_upload"
+        android:title="@string/action_export"
+        app:showAsAction="always">
+        <menu>
+            <item
+                android:id="@+id/action_export_clipboard"
+                android:title="@string/action_export" />
+            <item
+                android:id="@+id/action_export_file"
+                android:title="@string/action_export_file" />
+        </menu>
+    </item>
+    <item
+        android:id="@+id/action_clear"
+        android:title="@string/clear_profiles" />
+
 </menu>

+ 5 - 0
app/src/main/res/menu/add_route_menu.xml

@@ -6,4 +6,9 @@
         android:icon="@drawable/ic_baseline_add_road_24"
         android:title="@string/route_add"
         app:showAsAction="always" />
+    <item
+        android:id="@+id/action_reset_route"
+        android:title="@string/route_reset"
+        app:showAsAction="never" />
+
 </menu>

+ 2 - 2
app/src/main/res/values-zh-rCN/strings.xml

@@ -175,8 +175,8 @@
     <string name="action_from_link">添加订阅</string>
 
     <string name="action_scan_china_apps">扫描中国应用</string>
-    <string name="action_export_more">导出</string>
-    <string name="action_export_file">导出到文件</string>
+    <string name="action_export_more">导出</string>
+    <string name="action_export_file">导出到文件</string>
     <string name="action_export">导出到剪切板</string>
     <string name="action_import">从剪切板导入</string>
     <string name="action_import_file">从文件中导入</string>

+ 6 - 3
app/src/main/res/values/strings.xml

@@ -148,6 +148,8 @@
     <string name="empty_route_notice">Set some rules before saving</string>
     <string name="route_bypass_domain">Domain rule for %s</string>
     <string name="route_bypass_ip">IP rule for %s</string>
+    <string name="other">Other</string>
+    <string name="route_reset">Reset</string>
 
     <string name="route_opt_bypass_lan">Bypass LAN</string>
     <string name="route_opt_block_ads">Block ADs</string>
@@ -225,14 +227,14 @@
     <string name="circular_reference">Circular reference</string>
     <string name="circular_reference_sum">The route cannot contain itself.</string>
     <string name="profile_file">Profile File</string>
-
+    <string name="clear_profiles">Clear</string>
 
     <string name="action_create_group">Empty group</string>
     <string name="action_from_link">From subscription</string>
 
     <string name="action_scan_china_apps">Scan China apps</string>
-    <string name="action_export_more">Export</string>
-    <string name="action_export_file">Export to file</string>
+    <string name="action_export_more">Export</string>
+    <string name="action_export_file">Export to file</string>
     <string name="action_export">Export to Clipboard</string>
     <string name="action_import">Import from Clipboard</string>
     <string name="action_import_file">Import from file</string>
@@ -241,6 +243,7 @@
     <string name="action_export_err">Failed to export.</string>
     <string name="action_import_msg">Successfully import!</string>
     <string name="action_import_err">Failed to import.</string>
+    <string name="file_manager_missing">Please install a file manager like MiXplorer</string>
 
     <!-- share -->
     <string name="format_standard">Standard</string>