Browse Source

Replace vision with oss implementation & Fix link

世界 4 years ago
parent
commit
2e05f274bc

+ 2 - 17
app/build.gradle

@@ -150,25 +150,9 @@ dependencies {
     implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.2"
     implementation "com.takisoft.preferencex:preferencex-simplemenu:1.1.0"
     implementation "org.yaml:snakeyaml:1.28"
-
-
-    implementation ("androidx.camera:camera-camera2:$cameraxVersion") {
-        exclude group: "com.google.guava", module: "listenablefuture"
-    }
-    implementation ("androidx.camera:camera-lifecycle:$cameraxVersion") {
-        exclude group: "com.google.guava", module: "listenablefuture"
-    }
-    implementation ("androidx.camera:camera-view:1.0.0-alpha23") {
-        exclude group: "com.google.guava", module: "listenablefuture"
-    }
-
-    implementation "androidx.work:work-runtime-ktx:$workVersion"
     implementation "androidx.constraintlayout:constraintlayout:2.0.4"
     implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
 
-    implementation "com.google.mlkit:barcode-scanning:16.1.1"
-    implementation "com.google.zxing:core:3.4.1"
-
     implementation("com.simplecityapps:recyclerview-fastscroll:2.0.1") {
         exclude group: "androidx.recyclerview"
         exclude group: "androidx.appcompat"
@@ -176,7 +160,8 @@ dependencies {
     implementation ("org.smali:dexlib2:2.5.2") {
         exclude group: 'com.google.guava', module: 'guava'
     }
-    implementation("com.google.guava:guava:30.1.1-android")
+    implementation "com.google.guava:guava:30.1.1-android"
+    implementation "com.journeyapps:zxing-android-embedded:4.2.0"
 
     implementation "androidx.room:room-runtime:$roomVersion"
     kapt "androidx.room:room-compiler:$roomVersion"

+ 6 - 0
app/src/main/AndroidManifest.xml

@@ -3,6 +3,11 @@
     xmlns:tools="http://schemas.android.com/tools"
     package="io.nekohasekai.sagernet">
 
+    <uses-sdk
+        android:minSdkVersion="21"
+        tools:overrideLibrary="com.google.zxing.client.android" />
+
+
     <permission
         android:name="${applicationId}.SERVICE"
         android:protectionLevel="signature" />
@@ -40,6 +45,7 @@
         android:autoRevokePermissions="allowed"
         android:fullBackupContent="@xml/backup_descriptor"
         android:fullBackupOnly="true"
+        android:hardwareAccelerated="true"
         android:hasFragileUserData="true"
         android:icon="@mipmap/ic_launcher"
         android:label="@string/app_name"

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

@@ -51,6 +51,9 @@ object DataStore : OnPreferenceDataStoreChangeListener {
     }
 
     var selectedProxy by configurationStore.long(Key.PROFILE_ID)
+    var selectedGroup by configurationStore.long(Key.PROFILE_GROUP) {
+        SagerNet.currentProfile?.groupId ?: 0L
+    }
     var serviceMode by configurationStore.string(Key.SERVICE_MODE) { Key.MODE_VPN }
     var routeMode by configurationStore.string(Key.ROUTE_MODE) { RouteMode.ALL }
     var allowAccess by configurationStore.boolean(Key.ALLOW_ACCESS)
@@ -85,7 +88,7 @@ object DataStore : OnPreferenceDataStoreChangeListener {
         configurationStore.putString(key, "$value")
     }
 
-    var ipv6Route by configurationStore.boolean(Key.IPV6_ROUTE)
+    var ipv6Route by configurationStore.boolean(Key.IPV6_ROUTE) { true }
     var preferIpv6 by configurationStore.boolean(Key.PREFER_IPV6)
     var meteredNetwork by configurationStore.boolean(Key.METERED_NETWORK)
     var proxyApps by configurationStore.boolean(Key.PROXY_APPS)

+ 1 - 1
app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksFmt.kt

@@ -114,7 +114,7 @@ fun ShadowsocksBean.toUri(): String {
     }
 
     if (name.isNotBlank()) {
-        builder.fragment(name)
+        builder.encodedFragment(name.urlSafe())
     }
 
     return builder.toString().replace("https://", "ss://")

+ 0 - 104
app/src/main/java/io/nekohasekai/sagernet/ktx/Tasks.kt

@@ -1,104 +0,0 @@
-@file:Suppress("RedundantVisibilityModifier")
-package io.nekohasekai.sagernet.ktx
-
-/*
- * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
- */
-
-import com.google.android.gms.tasks.CancellationTokenSource
-import com.google.android.gms.tasks.RuntimeExecutionException
-import com.google.android.gms.tasks.Task
-import com.google.android.gms.tasks.TaskCompletionSource
-import com.google.common.util.concurrent.ListenableFuture
-import kotlinx.coroutines.*
-import java.util.concurrent.ExecutionException
-import kotlin.coroutines.*
-
-/**
- * Converts this deferred to the instance of [Task].
- * If deferred is cancelled then resulting task will be cancelled as well.
- */
-public fun <T> Deferred<T>.asTask(): Task<T> {
-    val cancellation = CancellationTokenSource()
-    val source = TaskCompletionSource<T>(cancellation.token)
-
-    invokeOnCompletion callback@{
-        if (it is CancellationException) {
-            cancellation.cancel()
-            return@callback
-        }
-
-        val t = getCompletionExceptionOrNull()
-        if (t == null) {
-            source.setResult(getCompleted())
-        } else {
-            source.setException(t as? Exception ?: RuntimeExecutionException(t))
-        }
-    }
-
-    return source.task
-}
-
-/**
- * Converts this task to an instance of [Deferred].
- * If task is cancelled then resulting deferred will be cancelled as well.
- */
-public fun <T> Task<T>.asDeferred(): Deferred<T> {
-    if (isComplete) {
-        val e = exception
-        return if (e == null) {
-            @Suppress("UNCHECKED_CAST")
-            CompletableDeferred<T>().apply { if (isCanceled) cancel() else complete(result as T) }
-        } else {
-            CompletableDeferred<T>().apply { completeExceptionally(e) }
-        }
-    }
-
-    val result = CompletableDeferred<T>()
-    addOnCompleteListener {
-        val e = it.exception
-        if (e == null) {
-            @Suppress("UNCHECKED_CAST")
-            if (isCanceled) result.cancel() else result.complete(it.result as T)
-        } else {
-            result.completeExceptionally(e)
-        }
-    }
-    return result
-}
-
-/**
- * Awaits for completion of the task without blocking a thread.
- *
- * This suspending function is cancellable.
- * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function
- * stops waiting for the completion stage and immediately resumes with [CancellationException].
- */
-public suspend fun <T> Task<T>.await(): T {
-    // fast path
-    if (isComplete) {
-        val e = exception
-        return if (e == null) {
-            if (isCanceled) {
-                throw CancellationException("Task $this was cancelled normally.")
-            } else {
-                @Suppress("UNCHECKED_CAST")
-                result as T
-            }
-        } else {
-            throw e
-        }
-    }
-
-    return suspendCancellableCoroutine { cont ->
-        addOnCompleteListener {
-            val e = exception
-            if (e == null) {
-                @Suppress("UNCHECKED_CAST")
-                if (isCanceled) cont.cancel() else cont.resume(result as T)
-            } else {
-                cont.resumeWithException(e)
-            }
-        }
-    }
-}

+ 11 - 3
app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt

@@ -309,8 +309,7 @@ class ConfigurationFragment : ToolbarFragment(R.layout.layout_group_list),
                     groupList = ArrayList(SagerDatabase.groupDao.allGroups())
                 }
 
-                val selectedGroup = ProfileManager.getProfile(DataStore.selectedProxy)?.groupId
-                    ?: selectedGroupIndex
+                val selectedGroup = DataStore.selectedGroup
                 if (selectedGroup != 0L) {
                     val selectedIndex = groupList.indexOfFirst { it.id == selectedGroup }
                     selectedGroupIndex = selectedIndex
@@ -455,6 +454,14 @@ class ConfigurationFragment : ToolbarFragment(R.layout.layout_group_list),
 
         }
 
+        override fun onStart() {
+            super.onStart()
+
+            runOnDefaultDispatcher {
+                DataStore.selectedGroup = proxyGroup.id
+            }
+        }
+
         inner class ConfigurationAdapter : RecyclerView.Adapter<ConfigurationHolder>(),
             ProfileManager.Listener {
 
@@ -615,7 +622,8 @@ class ConfigurationFragment : ToolbarFragment(R.layout.layout_group_list),
                     configurationIdList.clear()
                     configurationIdList.addAll(SagerDatabase.proxyDao.getIdsByGroup(proxyGroup.id))
 
-                    if (selected) {
+                    if (selected && !scrolled) {
+                        scrolled = true
                         val selectedProxy = DataStore.selectedProxy
                         val selectedProfileIndex = configurationIdList.indexOf(selectedProxy)
 

+ 144 - 121
app/src/main/java/io/nekohasekai/sagernet/ui/ScannerActivity.kt

@@ -21,119 +21,61 @@
 
 package io.nekohasekai.sagernet.ui
 
-import android.Manifest
 import android.content.Intent
 import android.content.pm.ShortcutManager
-import android.net.Uri
+import android.graphics.ImageDecoder
 import android.os.Build
 import android.os.Bundle
+import android.provider.MediaStore
+import android.view.KeyEvent
 import android.view.Menu
 import android.view.MenuItem
 import android.widget.Toast
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.appcompat.app.AppCompatActivity
-import androidx.camera.core.*
-import androidx.camera.lifecycle.ProcessCameraProvider
-import androidx.camera.view.PreviewView
 import androidx.core.content.getSystemService
-import androidx.lifecycle.lifecycleScope
-import androidx.work.await
-import com.google.mlkit.vision.barcode.Barcode
-import com.google.mlkit.vision.barcode.BarcodeScannerOptions
-import com.google.mlkit.vision.barcode.BarcodeScanning
-import com.google.mlkit.vision.common.InputImage
+import androidx.core.view.isGone
+import com.google.android.material.appbar.MaterialToolbar
+import com.google.zxing.BinaryBitmap
+import com.google.zxing.DecodeHintType
+import com.google.zxing.NotFoundException
+import com.google.zxing.RGBLuminanceSource
+import com.google.zxing.common.GlobalHistogramBinarizer
+import com.google.zxing.qrcode.QRCodeReader
+import com.journeyapps.barcodescanner.*
 import io.nekohasekai.sagernet.R
-import io.nekohasekai.sagernet.SagerNet
+import io.nekohasekai.sagernet.database.DataStore
 import io.nekohasekai.sagernet.database.ProfileManager
 import io.nekohasekai.sagernet.ktx.*
 import io.nekohasekai.sagernet.widget.ListHolderListener
-import kotlinx.coroutines.*
-
-class ScannerActivity : AppCompatActivity(), ImageAnalysis.Analyzer {
-    private val scanner = BarcodeScanning.getClient(BarcodeScannerOptions.Builder().apply {
-        setBarcodeFormats(Barcode.FORMAT_QR_CODE)
-    }.build())
-    private val imageAnalysis by lazy {
-        ImageAnalysis.Builder().apply {
-            setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
-            setBackgroundExecutor(Dispatchers.Default.asExecutor())
-        }.build().also { it.setAnalyzer(Dispatchers.Main.immediate.asExecutor(), this) }
-    }
 
-    @ExperimentalGetImage
-    override fun analyze(image: ImageProxy) {
-        val mediaImage = image.image ?: return
-        lifecycleScope.launchWhenCreated {
-            val result = try {
-                process() {
-                    InputImage.fromMediaImage(mediaImage,
-                        image.imageInfo.rotationDegrees)
-                }.also {
-                    if (it) imageAnalysis.clearAnalyzer()
-                }
-            } catch (_: CancellationException) {
-                return@launchWhenCreated
-            } catch (e: Exception) {
-                return@launchWhenCreated Logs.w(e)
-            } finally {
-                image.close()
-            }
-            if (result) onSupportNavigateUp()
-        }
-    }
+
+class ScannerActivity : AppCompatActivity(), BarcodeCallback {
+
+    lateinit var toolbar: MaterialToolbar
+    lateinit var capture: CaptureManager
+    lateinit var barcodeScanner: DecoratedBarcodeView
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         if (Build.VERSION.SDK_INT >= 25) getSystemService<ShortcutManager>()!!.reportShortcutUsed("scan")
         setContentView(R.layout.layout_scanner)
         ListHolderListener.setup(this)
-        setSupportActionBar(findViewById(R.id.toolbar))
-        supportActionBar!!.setDisplayHomeAsUpEnabled(true)
-        lifecycle.addObserver(scanner)
-        requestCamera.launch(Manifest.permission.CAMERA)
-    }
 
-    private val requestCamera =
-        registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
-            if (granted) lifecycleScope.launchWhenCreated {
-                val cameraProvider = ProcessCameraProvider.getInstance(this@ScannerActivity).await()
-                val selector = if (cameraProvider.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA)) {
-                    CameraSelector.DEFAULT_BACK_CAMERA
-                } else CameraSelector.DEFAULT_FRONT_CAMERA
-                val preview = Preview.Builder().build()
-                preview.setSurfaceProvider(findViewById<PreviewView>(R.id.barcode).surfaceProvider)
-                try {
-                    cameraProvider.bindToLifecycle(this@ScannerActivity,
-                        selector,
-                        preview,
-                        imageAnalysis)
-                } catch (e: IllegalArgumentException) {
-                    Logs.w(e)
-                    startImport()
-                }
-            } else permissionMissing()
-        }
-
-    private suspend inline fun process(crossinline image: () -> InputImage): Boolean {
-        val barcode = withContext(Dispatchers.Default) { scanner.process(image()).await() }
-        val results = parseProxies(barcode.mapNotNull { it.rawValue }.joinToString("\n"))
-        if (results.isNotEmpty()) {
-            val currentGroupId = SagerNet.currentProfile?.groupId ?: 0
-
-            for (result in results) {
-                ProfileManager.createProfile(currentGroupId, result)
-            }
+        toolbar = findViewById(R.id.toolbar)
+        setSupportActionBar(toolbar)
+        supportActionBar!!.setDisplayHomeAsUpEnabled(true)
+        supportActionBar!!.setHomeAsUpIndicator(R.drawable.ic_navigation_close)
 
-            return true
+        barcodeScanner = findViewById(R.id.barcode_scanner)
+        barcodeScanner.statusView.isGone = true
+        barcodeScanner.viewFinder.isGone = true
+        barcodeScanner.barcodeView.setDecoderFactory {
+            MixedDecoder(QRCodeReader())
         }
 
-        return false
-    }
-
-    private fun permissionMissing() {
-        Toast.makeText(this, R.string.add_profile_scanner_permission_required, Toast.LENGTH_SHORT)
-            .show()
-        startImport()
+        capture = CaptureManager(this, barcodeScanner)
+        barcodeScanner.decodeSingle(this)
     }
 
     override fun onCreateOptionsMenu(menu: Menu?): Boolean {
@@ -141,12 +83,76 @@ class ScannerActivity : AppCompatActivity(), ImageAnalysis.Analyzer {
         return true
     }
 
-    override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
-        R.id.action_import_clipboard -> {
-            startImport(true)
-            true
+    val importCodeFile = registerForActivityResult(ActivityResultContracts.GetMultipleContents()) {
+        runOnDefaultDispatcher {
+            try {
+                it.forEachTry { uri ->
+                    val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+                        ImageDecoder.decodeBitmap(ImageDecoder.createSource(contentResolver,
+                            uri)) { decoder, _, _ ->
+                            decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
+                            decoder.isMutableRequired = true
+                        }
+                    } else {
+                        MediaStore.Images.Media.getBitmap(contentResolver, uri)
+                    }
+                    val intArray = IntArray(bitmap.width * bitmap.height)
+                    bitmap.getPixels(intArray,
+                        0,
+                        bitmap.width,
+                        0,
+                        0, bitmap.width, bitmap.height)
+
+                    val source = RGBLuminanceSource(bitmap.width, bitmap.height, intArray)
+                    val qrReader = QRCodeReader()
+                    try {
+                        val result = try {
+                            qrReader.decode(BinaryBitmap(GlobalHistogramBinarizer(source)),
+                                mapOf(
+                                    DecodeHintType.TRY_HARDER to true
+                                ))
+                        } catch (e: NotFoundException) {
+                            qrReader.decode(BinaryBitmap(GlobalHistogramBinarizer(source.invert())),
+                                mapOf(
+                                    DecodeHintType.TRY_HARDER to true
+                                ))
+                        }
+
+                        val results = parseProxies(result.text ?: "")
+
+                        if (results.isNotEmpty()) {
+                            onMainDispatcher {
+                                finish()
+                            }
+                            val currentGroupId = DataStore.selectedGroup
+                            for (profile in results) {
+                                ProfileManager.createProfile(currentGroupId, profile)
+                            }
+                        } else {
+                            Toast.makeText(app, R.string.action_import_err, Toast.LENGTH_SHORT)
+                                .show()
+                        }
+                    } catch (e: Throwable) {
+                        Logs.w(e)
+                        onMainDispatcher {
+                            Toast.makeText(app, R.string.action_import_err, Toast.LENGTH_SHORT)
+                                .show()
+                        }
+                    }
+                }
+            } catch (e: Exception) {
+                Logs.w(e)
+
+                onMainDispatcher {
+                    Toast.makeText(app, e.readableMessage, Toast.LENGTH_LONG).show()
+                }
+            }
         }
-        else -> false
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        importCodeFile.launch("image/*")
+        return true
     }
 
     /**
@@ -155,38 +161,55 @@ class ScannerActivity : AppCompatActivity(), ImageAnalysis.Analyzer {
     override fun shouldUpRecreateTask(targetIntent: Intent?) =
         super.shouldUpRecreateTask(targetIntent) || isTaskRoot
 
-    private var finished = false
-    override fun onSupportNavigateUp(): Boolean {
-        if (finished) return false
-        finished = true
-        return super.onSupportNavigateUp()
+    override fun onResume() {
+        super.onResume()
+        capture.onResume()
     }
 
-    private fun startImport(manual: Boolean = false) =
-        (if (manual) import else importFinish).launch("image/*")
+    override fun onPause() {
+        super.onPause()
+        capture.onPause()
+    }
 
-    private val import =
-        registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { importOrFinish(it) }
-    private val importFinish =
-        registerForActivityResult(ActivityResultContracts.GetMultipleContents()) {
-            importOrFinish(it, true)
-        }
+    override fun onDestroy() {
+        super.onDestroy()
+        capture.onDestroy()
+    }
 
-    private fun importOrFinish(dataUris: List<Uri>, finish: Boolean = false) {
-        if (dataUris.isNotEmpty()) GlobalScope.launch(Dispatchers.Main.immediate) {
-            onSupportNavigateUp()
-            val feature = SagerNet.currentProfile
-            try {
-                var success = false
-                dataUris.forEachTry { uri ->
-                    if (process { InputImage.fromFilePath(app, uri) }) success = true
+    override fun onSaveInstanceState(outState: Bundle) {
+        super.onSaveInstanceState(outState)
+        capture.onSaveInstanceState(outState)
+    }
+
+    override fun onRequestPermissionsResult(
+        requestCode: Int,
+        permissions: Array<String>,
+        grantResults: IntArray,
+    ) {
+        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+        capture.onRequestPermissionsResult(requestCode, permissions, grantResults)
+    }
+
+    override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
+        return barcodeScanner.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event)
+    }
+
+    override fun barcodeResult(result: BarcodeResult) {
+        finish()
+        val text = result.result.text
+        runOnDefaultDispatcher {
+            val results = parseProxies(text)
+            if (results.isNotEmpty()) {
+                val currentGroupId = DataStore.selectedGroup
+
+                for (profile in results) {
+                    ProfileManager.createProfile(currentGroupId, profile)
                 }
-                Toast.makeText(app,
-                    if (success) R.string.action_import_msg else R.string.action_import_err,
-                    Toast.LENGTH_SHORT).show()
-            } catch (e: Exception) {
-                Toast.makeText(app, e.readableMessage, Toast.LENGTH_LONG).show()
+            } else {
+                Toast.makeText(app, R.string.action_import_err, Toast.LENGTH_SHORT)
+                    .show()
             }
-        } else if (finish) onSupportNavigateUp()
+        }
     }
-}
+
+}

+ 8 - 6
app/src/main/res/layout/layout_scanner.xml

@@ -1,12 +1,14 @@
 <?xml version="1.0" encoding="utf-8"?>
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:orientation="vertical">
     <include layout="@layout/layout_appbar" />
-    <androidx.camera.view.PreviewView
-        android:id="@+id/barcode"
-        android:layout_width="fill_parent"
-        android:layout_height="fill_parent"/>
+    <com.journeyapps.barcodescanner.DecoratedBarcodeView
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:id="@+id/barcode_scanner"
+        app:zxing_preview_scaling_strategy="centerCrop"
+        app:zxing_use_texture_view="true"/>
 </LinearLayout>

+ 2 - 1
app/src/main/res/xml/global_preferences.xml

@@ -41,7 +41,7 @@
 
     <PreferenceCategory app:title="@string/cag_route">
         <com.takisoft.preferencex.SimpleMenuPreference
-            app:defaultValue="bypass-lan-china"
+            app:defaultValue="bypaass-lan"
             app:entries="@array/route_entry"
             app:entryValues="@array/route_value"
             app:icon="@drawable/ic_maps_directions"
@@ -49,6 +49,7 @@
             app:title="@string/route_list"
             app:useSimpleSummaryProvider="true" />
         <SwitchPreference
+            app:defaultValue="true"
             app:icon="@drawable/ic_image_looks_6"
             app:key="ipv6Route"
             app:summary="@string/ipv6_summary"