diff --git a/app/build.gradle b/app/build.gradle
index 9268bb0fe..2e4307723 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -126,6 +126,9 @@ dependencies {
//Glide
implementation 'com.github.bumptech.glide:glide:4.9.0'
+ //二维码
+ implementation 'cn.bingoogolapple:bga-qrcode-zxing:1.3.6'
+
//颜色选择
implementation 'com.jaredrummler:colorpicker:1.1.0'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 9682d9f2e..91282f69e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -44,6 +44,7 @@
+
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/help/storage/Restore.kt b/app/src/main/java/io/legado/app/help/storage/Restore.kt
index aafb71f5c..021b8585c 100644
--- a/app/src/main/java/io/legado/app/help/storage/Restore.kt
+++ b/app/src/main/java/io/legado/app/help/storage/Restore.kt
@@ -26,7 +26,7 @@ object Restore {
}
fun importYueDuData(context: Context) {
- val yuedu = File(getSdPath(), "YueDu")
+ val yuedu = File(FileUtils.getSdPath(), "YueDu")
val jsonPath = JsonPath.using(
Configuration.builder()
.options(Option.SUPPRESS_EXCEPTIONS)
diff --git a/app/src/main/java/io/legado/app/ui/qrcode/QrCodeActivity.kt b/app/src/main/java/io/legado/app/ui/qrcode/QrCodeActivity.kt
new file mode 100644
index 000000000..d77022022
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/qrcode/QrCodeActivity.kt
@@ -0,0 +1,112 @@
+package io.legado.app.ui.qrcode
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuItem
+import androidx.lifecycle.AndroidViewModel
+import cn.bingoogolapple.qrcode.core.QRCodeView
+import io.legado.app.R
+import io.legado.app.base.BaseActivity
+import io.legado.app.help.permission.Permissions
+import io.legado.app.help.permission.PermissionsCompat
+import io.legado.app.utils.FileUtils
+import io.legado.app.utils.getViewModel
+import kotlinx.android.synthetic.main.activity_qrcode_capture.*
+import kotlinx.android.synthetic.main.view_title_bar.*
+
+class QrCodeActivity : BaseActivity(), QRCodeView.Delegate {
+ override val viewModel: AndroidViewModel
+ get() = getViewModel(AndroidViewModel::class.java)
+ override val layoutID: Int
+ get() = R.layout.activity_qrcode_capture
+
+ private val requestQrImage = 202
+ private var flashlightIsOpen: Boolean = false
+
+ override fun onViewModelCreated(viewModel: AndroidViewModel, savedInstanceState: Bundle?) {
+ setSupportActionBar(toolbar)
+ zxingview.setDelegate(this)
+ fab_flashlight.setOnClickListener {
+ if (flashlightIsOpen) {
+ flashlightIsOpen = false
+ zxingview.closeFlashlight()
+ } else {
+ flashlightIsOpen = true
+ zxingview.openFlashlight()
+ }
+ }
+ }
+
+ override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.qr_code_scan, menu)
+ return super.onCompatCreateOptionsMenu(menu)
+ }
+
+ override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ R.id.action_choose_from_gallery -> {
+ val intent = Intent(Intent.ACTION_GET_CONTENT)
+ intent.addCategory(Intent.CATEGORY_OPENABLE)
+ intent.type = "image/*"
+ startActivityForResult(intent, requestQrImage)
+ }
+ }
+ return super.onCompatOptionsItemSelected(item)
+ }
+
+ override fun onStart() {
+ super.onStart()
+ startCamera()
+ }
+
+ private fun startCamera() {
+ PermissionsCompat.Builder(this)
+ .addPermissions(*Permissions.Group.CAMERA)
+ .rationale(R.string.qr_per)
+ .onGranted {
+ zxingview.startCamera() // 打开后置摄像头开始预览,但是并未开始识别
+ zxingview.startSpotAndShowRect() // 显示扫描框,并开始识别
+ }.build()
+ }
+
+ override fun onStop() {
+ zxingview.stopCamera() // 关闭摄像头预览,并且隐藏扫描框
+ super.onStop()
+ }
+
+ override fun onDestroy() {
+ zxingview.onDestroy() // 销毁二维码扫描控件
+ super.onDestroy()
+ }
+
+ override fun onScanQRCodeSuccess(result: String) {
+ val intent = Intent()
+ intent.putExtra("result", result)
+ setResult(RESULT_OK, intent)
+ finish()
+ }
+
+ override fun onCameraAmbientBrightnessChanged(isDark: Boolean) {
+
+ }
+
+ override fun onScanQRCodeOpenCameraError() {
+
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ data?.let {
+ zxingview.startSpotAndShowRect() // 显示扫描框,并开始识别
+
+ if (resultCode == Activity.RESULT_OK && requestCode == requestQrImage) {
+ val picturePath = FileUtils.getPath(this, it.data)
+ // 本来就用到 QRCodeView 时可直接调 QRCodeView 的方法,走通用的回调
+ zxingview.decodeQRCode(picturePath)
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/utils/FileUtils.kt b/app/src/main/java/io/legado/app/utils/FileUtils.kt
index 493347429..8c5ba3823 100644
--- a/app/src/main/java/io/legado/app/utils/FileUtils.kt
+++ b/app/src/main/java/io/legado/app/utils/FileUtils.kt
@@ -1,5 +1,235 @@
package io.legado.app.utils
+import android.annotation.TargetApi
+import android.content.ContentUris
+import android.content.Context
+import android.net.Uri
+import android.os.Build
import android.os.Environment
+import android.os.storage.StorageManager
+import android.provider.DocumentsContract
+import android.provider.MediaStore
+import android.util.Log
+import androidx.core.content.ContextCompat
+import java.io.File
+import java.io.IOException
+import java.lang.reflect.Array
+import java.util.*
-fun getSdPath() = Environment.getExternalStorageDirectory().absolutePath
+
+object FileUtils {
+ fun getSdPath() = Environment.getExternalStorageDirectory().absolutePath
+
+ fun getFileByPath(filePath: String): File? {
+ return if (isSpace(filePath)) null else File(filePath)
+ }
+
+ fun isSpace(s: String?): Boolean {
+ if (s == null) return true
+ var i = 0
+ val len = s.length
+ while (i < len) {
+ if (!Character.isWhitespace(s[i])) {
+ return false
+ }
+ ++i
+ }
+ return true
+ }
+
+ fun getSdCardPath(): String {
+ var sdCardDirectory = Environment.getExternalStorageDirectory().absolutePath
+
+ try {
+ sdCardDirectory = File(sdCardDirectory).canonicalPath
+ } catch (ioe: IOException) {
+ }
+
+ return sdCardDirectory
+ }
+
+ fun getStorageData(pContext: Context): ArrayList? {
+
+ val storageManager = pContext.getSystemService(Context.STORAGE_SERVICE) as StorageManager
+
+ try {
+ val getVolumeList = storageManager.javaClass.getMethod("getVolumeList")
+
+ val storageValumeClazz = Class.forName("android.os.storage.StorageVolume")
+ val getPath = storageValumeClazz.getMethod("getPath")
+
+ val invokeVolumeList = getVolumeList.invoke(storageManager)
+ val length = Array.getLength(invokeVolumeList)
+
+ val list = ArrayList()
+ for (i in 0 until length) {
+ val storageValume = Array.get(invokeVolumeList, i)//得到StorageVolume对象
+ val path = getPath.invoke(storageValume) as String
+
+ list.add(path)
+ }
+ return list
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+
+ return null
+ }
+
+
+ fun getExtSdCardPaths(con: Context): ArrayList {
+ val paths = ArrayList()
+ val files = ContextCompat.getExternalFilesDirs(con, "external")
+ val firstFile = files[0]
+ for (file in files) {
+ if (file != null && file != firstFile) {
+ val index = file.absolutePath.lastIndexOf("/Android/data")
+ if (index < 0) {
+ Log.w("", "Unexpected external file dir: " + file.absolutePath)
+ } else {
+ var path = file.absolutePath.substring(0, index)
+ try {
+ path = File(path).canonicalPath
+ } catch (e: IOException) {
+ // Keep non-canonical path.
+ }
+
+ paths.add(path)
+ }
+ }
+ }
+ return paths
+ }
+
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ fun getPath(context: Context, uri: Uri): String? {
+ val isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
+
+ // DocumentProvider
+ if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
+ // ExternalStorageProvider
+ if (isExternalStorageDocument(uri)) {
+ val docId = DocumentsContract.getDocumentId(uri)
+ val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
+ val type = split[0]
+
+ if ("primary".equals(type, ignoreCase = true)) {
+ return Environment.getExternalStorageDirectory().toString() + "/" + split[1]
+ }
+
+ } else if (isDownloadsDocument(uri)) {
+ val id = DocumentsContract.getDocumentId(uri)
+ val split = id.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
+ val type = split[0]
+ if ("raw".equals(
+ type,
+ ignoreCase = true
+ )
+ ) { //处理某些机型(比如Goole Pixel )ID是raw:/storage/emulated/0/Download/c20f8664da05ab6b4644913048ea8c83.mp4
+ return split[1]
+ }
+
+ val contentUri = ContentUris.withAppendedId(
+ Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id)
+ )
+
+ return getDataColumn(context, contentUri, null, null)
+ } else if (isMediaDocument(uri)) {
+ val docId = DocumentsContract.getDocumentId(uri)
+ val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
+ val type = split[0]
+
+ var contentUri: Uri? = null
+ if ("image" == type) {
+ contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
+ } else if ("video" == type) {
+ contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
+ } else if ("audio" == type) {
+ contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
+ }
+
+ val selection = "_id=?"
+ val selectionArgs = arrayOf(split[1])
+
+ return getDataColumn(context, contentUri, selection, selectionArgs)
+ }// MediaProvider
+ // DownloadsProvider
+ } else if ("content".equals(uri.scheme!!, ignoreCase = true)) {
+
+ // Return the remote address
+ return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn(
+ context,
+ uri,
+ null,
+ null
+ )
+
+ } else if ("file".equals(uri.scheme!!, ignoreCase = true)) {
+ return uri.path
+ }// File
+ // MediaStore (and general)
+
+ return null
+ }
+
+ fun getDataColumn(
+ context: Context, uri: Uri?, selection: String?,
+ selectionArgs: kotlin.Array?
+ ): String? {
+
+ val column = "_data"
+ val projection = arrayOf(column)
+
+ try {
+ context.contentResolver.query(
+ uri!!,
+ projection,
+ selection,
+ selectionArgs,
+ null
+ )!!.use { cursor ->
+ if (cursor != null && cursor.moveToFirst()) {
+ val index = cursor.getColumnIndexOrThrow(column)
+ return cursor.getString(index)
+ }
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+
+ return null
+ }
+
+ /**
+ * @param uri The Uri to check.
+ * @return Whether the Uri authority is ExternalStorageProvider.
+ */
+ fun isExternalStorageDocument(uri: Uri): Boolean {
+ return "com.android.externalstorage.documents" == uri.authority
+ }
+
+ /**
+ * @param uri The Uri to check.
+ * @return Whether the Uri authority is DownloadsProvider.
+ */
+ fun isDownloadsDocument(uri: Uri): Boolean {
+ return "com.android.providers.downloads.documents" == uri.authority
+ }
+
+ /**
+ * @param uri The Uri to check.
+ * @return Whether the Uri authority is MediaProvider.
+ */
+ fun isMediaDocument(uri: Uri): Boolean {
+ return "com.android.providers.media.documents" == uri.authority
+ }
+
+ /**
+ * @param uri The Uri to check.
+ * @return Whether the Uri authority is Google Photos.
+ */
+ fun isGooglePhotosUri(uri: Uri): Boolean {
+ return "com.google.android.apps.photos.content" == uri.authority
+ }
+
+}
diff --git a/app/src/main/res/layout/activity_qrcode_capture.xml b/app/src/main/res/layout/activity_qrcode_capture.xml
new file mode 100644
index 000000000..71df5842b
--- /dev/null
+++ b/app/src/main/res/layout/activity_qrcode_capture.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/qr_code_scan.xml b/app/src/main/res/menu/qr_code_scan.xml
new file mode 100644
index 000000000..00bb7ec15
--- /dev/null
+++ b/app/src/main/res/menu/qr_code_scan.xml
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file