mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-05-28 12:30:12 +02:00
fix(downloader): major improvement (#110)
* fix: use download manager when no write permissions * fix: download manager & config - use coroutines * fix(download_manager): scan media * fix: download server blocking request * fix(download/manager_receiver): IO dispatcher - add debug messages * build: packaging options - add armv7 wildcard filter * feat: build notices - add notice for debug/lspatch builds and different package name - add project author watermark * fix(downloader): manage external storage permission * fix(download/receiver): scan media file * fix(context): storage permission for legacy devices
This commit is contained in:
parent
19ec7463b0
commit
d8625b4e80
@ -54,14 +54,7 @@ android {
|
||||
abiFilters "armeabi-v7a"
|
||||
}
|
||||
packagingOptions {
|
||||
exclude 'lib/armeabi-v7a/libswscale_neon.so'
|
||||
exclude 'lib/armeabi-v7a/libswresample_neon.so'
|
||||
exclude 'lib/armeabi-v7a/libffmpegkit_armv7a_neon.so'
|
||||
exclude 'lib/armeabi-v7a/libavutil_neon.so'
|
||||
exclude 'lib/armeabi-v7a/libavformat_neon.so'
|
||||
exclude 'lib/armeabi-v7a/libavfilter_neon.so'
|
||||
exclude 'lib/armeabi-v7a/libavdevice_neon.so'
|
||||
exclude 'lib/armeabi-v7a/libavcodec_neon.so'
|
||||
exclude 'lib/armeabi-v7a/*_neon.so'
|
||||
}
|
||||
dimension "release"
|
||||
}
|
||||
|
@ -10,8 +10,11 @@
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<application
|
||||
android:usesCleartextTraffic="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:label="@string/app_name"
|
||||
tools:targetApi="31"
|
||||
android:icon="@mipmap/launcher_icon">
|
||||
|
@ -1,8 +1,15 @@
|
||||
package me.rhunk.snapenhance
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.Settings
|
||||
import me.rhunk.snapenhance.bridge.TranslationWrapper
|
||||
import me.rhunk.snapenhance.download.DownloadTaskManager
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
* Used to store objects between activities and receivers
|
||||
@ -11,6 +18,24 @@ object SharedContext {
|
||||
lateinit var downloadTaskManager: DownloadTaskManager
|
||||
lateinit var translation: TranslationWrapper
|
||||
|
||||
private fun askForStoragePermission(context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
|
||||
intent.addCategory("android.intent.category.DEFAULT")
|
||||
intent.data = android.net.Uri.parse("package:${context.packageName}")
|
||||
if (context !is Activity) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
exitProcess(0)
|
||||
}
|
||||
if (context !is Activity) {
|
||||
Logger.log("Storage permission not granted, exiting")
|
||||
exitProcess(0)
|
||||
}
|
||||
context.requestPermissions(arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE, android.Manifest.permission.READ_EXTERNAL_STORAGE), 0)
|
||||
}
|
||||
|
||||
fun ensureInitialized(context: Context) {
|
||||
if (!this::downloadTaskManager.isInitialized) {
|
||||
downloadTaskManager = DownloadTaskManager().apply {
|
||||
@ -22,5 +47,29 @@ object SharedContext {
|
||||
loadFromContext(context)
|
||||
}
|
||||
}
|
||||
|
||||
//ask for storage permission
|
||||
val hasStoragePermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
Environment.isExternalStorageManager()
|
||||
} else {
|
||||
context.checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
if (hasStoragePermission) return
|
||||
|
||||
if (context !is Activity) {
|
||||
askForStoragePermission(context)
|
||||
return
|
||||
}
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle("Storage permission")
|
||||
.setMessage("App needs storage permission to download files and save them to your device. Please allow it in the next screen.")
|
||||
.setPositiveButton("Grant") { _, _ ->
|
||||
askForStoragePermission(context)
|
||||
}
|
||||
.setNegativeButton("Cancel") { _, _ ->
|
||||
exitProcess(0)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
@ -1,14 +1,14 @@
|
||||
package me.rhunk.snapenhance.download
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.media.MediaScannerConnection
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.widget.Toast
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.joinAll
|
||||
@ -16,6 +16,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import me.rhunk.snapenhance.Constants
|
||||
import me.rhunk.snapenhance.Logger
|
||||
import me.rhunk.snapenhance.SharedContext
|
||||
import me.rhunk.snapenhance.data.FileType
|
||||
import me.rhunk.snapenhance.download.data.DownloadRequest
|
||||
import me.rhunk.snapenhance.download.data.InputMedia
|
||||
@ -23,9 +24,8 @@ import me.rhunk.snapenhance.download.data.MediaEncryptionKeyPair
|
||||
import me.rhunk.snapenhance.download.data.PendingDownload
|
||||
import me.rhunk.snapenhance.download.enums.DownloadMediaType
|
||||
import me.rhunk.snapenhance.download.enums.DownloadStage
|
||||
import me.rhunk.snapenhance.SharedContext
|
||||
import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper
|
||||
import me.rhunk.snapenhance.util.download.RemoteMediaResolver
|
||||
import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.net.HttpURLConnection
|
||||
@ -113,6 +113,7 @@ class DownloadManagerReceiver : BroadcastReceiver() {
|
||||
return file
|
||||
}
|
||||
|
||||
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
||||
private suspend fun saveMediaToGallery(inputFile: File, pendingDownload: PendingDownload) {
|
||||
if (coroutineContext.job.isCancelled) return
|
||||
|
||||
@ -122,11 +123,24 @@ class DownloadManagerReceiver : BroadcastReceiver() {
|
||||
longToast(translation.format("failed_gallery_toast", "error" to "Unknown media type"))
|
||||
return
|
||||
}
|
||||
|
||||
val outputFile = File(pendingDownload.outputPath + "." + fileType.fileExtension).also { createNeededDirectories(it) }
|
||||
|
||||
inputFile.copyTo(outputFile, overwrite = true)
|
||||
|
||||
MediaScannerConnection.scanFile(context, arrayOf(outputFile.absolutePath), null, null)
|
||||
pendingDownload.outputFile = outputFile.absolutePath
|
||||
pendingDownload.downloadStage = DownloadStage.SAVED
|
||||
|
||||
runCatching {
|
||||
val contentUri = Uri.fromFile(outputFile)
|
||||
val mediaScanIntent = Intent("android.intent.action.MEDIA_SCANNER_SCAN_FILE")
|
||||
mediaScanIntent.setData(contentUri)
|
||||
context.sendBroadcast(mediaScanIntent)
|
||||
}.onFailure {
|
||||
Logger.error("Failed to scan media file", it)
|
||||
longToast(translation.format("failed_gallery_toast", "error" to it.toString()))
|
||||
}
|
||||
|
||||
Logger.debug("download complete")
|
||||
|
||||
//print the path of the saved media
|
||||
val parentName = outputFile.parentFile?.parentFile?.absolutePath?.let {
|
||||
@ -136,9 +150,6 @@ class DownloadManagerReceiver : BroadcastReceiver() {
|
||||
shortToast(
|
||||
translation.format("saved_toast", "path" to outputFile.absolutePath.replace(parentName ?: "", ""))
|
||||
)
|
||||
|
||||
pendingDownload.outputFile = outputFile.absolutePath
|
||||
pendingDownload.downloadStage = DownloadStage.SAVED
|
||||
}.onFailure {
|
||||
Logger.error(it)
|
||||
longToast(translation.format("failed_gallery_toast", "error" to it.toString()))
|
||||
@ -254,10 +265,11 @@ class DownloadManagerReceiver : BroadcastReceiver() {
|
||||
return newFile
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action != DOWNLOAD_ACTION) return
|
||||
this.context = context
|
||||
Logger.debug("onReceive download")
|
||||
|
||||
SharedContext.ensureInitialized(context)
|
||||
|
||||
val downloadRequest = DownloadRequest.fromBundle(intent.extras!!)
|
||||
@ -273,7 +285,7 @@ class DownloadManagerReceiver : BroadcastReceiver() {
|
||||
return
|
||||
}
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val pendingDownloadObject = PendingDownload.fromBundle(intent.extras!!)
|
||||
|
||||
SharedContext.downloadTaskManager.addTask(pendingDownloadObject)
|
||||
@ -287,6 +299,7 @@ class DownloadManagerReceiver : BroadcastReceiver() {
|
||||
val downloadedMedias = downloadInputMedias(downloadRequest).map {
|
||||
it.key to DownloadedFile(it.value, FileType.fromFile(it.value))
|
||||
}.toMap().toMutableMap()
|
||||
Logger.debug("downloaded ${downloadedMedias.size} medias")
|
||||
|
||||
var shouldMergeOverlay = downloadRequest.shouldMergeOverlay
|
||||
|
||||
|
@ -152,7 +152,8 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
|
||||
if (uri.scheme == "file") {
|
||||
return@let suspendCoroutine<String> { continuation ->
|
||||
context.downloadServer.ensureServerStarted {
|
||||
val url = putDownloadableContent(Paths.get(uri.path).inputStream())
|
||||
val file = Paths.get(uri.path).toFile()
|
||||
val url = putDownloadableContent(file.inputStream(), file.length())
|
||||
continuation.resumeWith(Result.success(url))
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.res.ColorStateList
|
||||
import android.os.Bundle
|
||||
import android.text.Html
|
||||
import android.text.InputType
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@ -12,6 +13,7 @@ import android.widget.ImageButton
|
||||
import android.widget.Switch
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import me.rhunk.snapenhance.BuildConfig
|
||||
import me.rhunk.snapenhance.R
|
||||
import me.rhunk.snapenhance.SharedContext
|
||||
import me.rhunk.snapenhance.bridge.ConfigWrapper
|
||||
@ -104,6 +106,26 @@ class ConfigActivity : Activity() {
|
||||
|
||||
val propertyListLayout = findViewById<ViewGroup>(R.id.property_list)
|
||||
|
||||
if (intent.getBooleanExtra("lspatched", false) ||
|
||||
applicationInfo.packageName != "me.rhunk.snapenhance" ||
|
||||
BuildConfig.DEBUG) {
|
||||
propertyListLayout.addView(
|
||||
layoutInflater.inflate(
|
||||
R.layout.config_activity_debug_item,
|
||||
propertyListLayout,
|
||||
false
|
||||
).apply {
|
||||
findViewById<TextView>(R.id.debug_item_content).apply {
|
||||
text = Html.fromHtml(
|
||||
"You are using a <u><b>debug/unofficial</b></u> build!\n" +
|
||||
"Please consider downloading stable builds from <a href=\"https://github.com/rhunk/SnapEnhance\">GitHub</a>.",
|
||||
Html.FROM_HTML_MODE_COMPACT
|
||||
)
|
||||
movementMethod = android.text.method.LinkMovementMethod.getInstance()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var currentCategory: ConfigCategory? = null
|
||||
|
||||
config.entries().forEach { (property, value) ->
|
||||
@ -248,5 +270,12 @@ class ConfigActivity : Activity() {
|
||||
propertyListLayout.addView(configItem)
|
||||
addSeparator()
|
||||
}
|
||||
|
||||
propertyListLayout.addView(layoutInflater.inflate(R.layout.config_activity_debug_item, propertyListLayout, false).apply {
|
||||
findViewById<TextView>(R.id.debug_item_content).apply {
|
||||
text = Html.fromHtml("Made by rhunk on <a href=\"https://github.com/rhunk/SnapEnhance\">GitHub</a>", Html.FROM_HTML_MODE_COMPACT)
|
||||
movementMethod = android.text.method.LinkMovementMethod.getInstance()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ import me.rhunk.snapenhance.BuildConfig
|
||||
import me.rhunk.snapenhance.Constants
|
||||
import me.rhunk.snapenhance.ui.config.ConfigActivity
|
||||
import me.rhunk.snapenhance.ui.menu.AbstractMenu
|
||||
import java.io.File
|
||||
|
||||
|
||||
@SuppressLint("DiscouragedApi")
|
||||
@ -49,6 +50,7 @@ class SettingsGearInjector : AbstractMenu() {
|
||||
val intent = Intent().apply {
|
||||
setClassName(BuildConfig.APPLICATION_ID, ConfigActivity::class.java.name)
|
||||
}
|
||||
intent.putExtra("lspatched", File(context.cacheDir, "lspatch/origin").exists())
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
|
@ -1,22 +1,27 @@
|
||||
package me.rhunk.snapenhance.util.download
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import me.rhunk.snapenhance.Logger
|
||||
import me.rhunk.snapenhance.Logger.debug
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.io.PrintWriter
|
||||
import java.net.ServerSocket
|
||||
import java.net.Socket
|
||||
import java.net.SocketTimeoutException
|
||||
import java.util.Locale
|
||||
import java.util.StringTokenizer
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ThreadLocalRandom
|
||||
|
||||
class DownloadServer {
|
||||
class DownloadServer(
|
||||
private val timeout: Int = 10000
|
||||
) {
|
||||
private val port = ThreadLocalRandom.current().nextInt(10000, 65535)
|
||||
|
||||
private val cachedData = ConcurrentHashMap<String, InputStream>()
|
||||
private val cachedData = ConcurrentHashMap<String, Pair<InputStream, Long>>()
|
||||
private var serverSocket: ServerSocket? = null
|
||||
|
||||
fun ensureServerStarted(callback: DownloadServer.() -> Unit) {
|
||||
@ -24,28 +29,37 @@ class DownloadServer {
|
||||
callback(this)
|
||||
return
|
||||
}
|
||||
Thread {
|
||||
try {
|
||||
debug("started web server on 127.0.0.1:$port")
|
||||
serverSocket = ServerSocket(port)
|
||||
callback(this)
|
||||
while (!serverSocket!!.isClosed) {
|
||||
try {
|
||||
val socket = serverSocket!!.accept()
|
||||
Thread { handleRequest(socket) }.start()
|
||||
} catch (e: Throwable) {
|
||||
Logger.xposedLog(e)
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
Logger.debug("starting download server on port $port")
|
||||
serverSocket = ServerSocket(port)
|
||||
serverSocket!!.soTimeout = timeout
|
||||
callback(this@DownloadServer)
|
||||
while (!serverSocket!!.isClosed) {
|
||||
try {
|
||||
val socket = serverSocket!!.accept()
|
||||
launch(Dispatchers.IO) {
|
||||
handleRequest(socket)
|
||||
}
|
||||
} catch (e: SocketTimeoutException) {
|
||||
serverSocket?.close()
|
||||
serverSocket = null
|
||||
Logger.debug("download server closed")
|
||||
break;
|
||||
} catch (e: Exception) {
|
||||
Logger.error("failed to handle request", e)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.xposedLog(e)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
|
||||
fun putDownloadableContent(inputStream: InputStream): String {
|
||||
fun close() {
|
||||
serverSocket?.close()
|
||||
}
|
||||
|
||||
fun putDownloadableContent(inputStream: InputStream, size: Long): String {
|
||||
val key = System.nanoTime().toString(16)
|
||||
cachedData[key] = inputStream
|
||||
cachedData[key] = inputStream to size
|
||||
return "http://127.0.0.1:$port/$key"
|
||||
}
|
||||
|
||||
@ -96,14 +110,11 @@ class DownloadServer {
|
||||
with(writer) {
|
||||
println("HTTP/1.1 200 OK")
|
||||
println("Content-type: " + "application/octet-stream")
|
||||
println("Content-length: " + requestedData.second)
|
||||
println()
|
||||
flush()
|
||||
}
|
||||
val buffer = ByteArray(1024)
|
||||
var bytesRead: Int
|
||||
while (requestedData.read(buffer).also { bytesRead = it } != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead)
|
||||
}
|
||||
requestedData.first.copyTo(outputStream)
|
||||
outputStream.flush()
|
||||
cachedData.remove(fileRequested)
|
||||
close()
|
||||
|
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/primaryBackground"
|
||||
android:layout_width="match_parent"
|
||||
|
18
app/src/main/res/layout/config_activity_debug_item.xml
Normal file
18
app/src/main/res/layout/config_activity_debug_item.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/debug_item_content"
|
||||
android:text=""
|
||||
android:linksClickable="true"
|
||||
android:textSize="15sp"
|
||||
android:textColor="@color/primaryText"
|
||||
android:gravity="center" />
|
||||
|
||||
</LinearLayout>
|
Loading…
x
Reference in New Issue
Block a user