mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-05-29 21:10:20 +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"
|
abiFilters "armeabi-v7a"
|
||||||
}
|
}
|
||||||
packagingOptions {
|
packagingOptions {
|
||||||
exclude 'lib/armeabi-v7a/libswscale_neon.so'
|
exclude 'lib/armeabi-v7a/*_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'
|
|
||||||
}
|
}
|
||||||
dimension "release"
|
dimension "release"
|
||||||
}
|
}
|
||||||
|
@ -10,8 +10,11 @@
|
|||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
tools:ignore="ScopedStorage" />
|
tools:ignore="ScopedStorage" />
|
||||||
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||||
|
tools:ignore="ScopedStorage" />
|
||||||
<application
|
<application
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
|
android:requestLegacyExternalStorage="true"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
tools:targetApi="31"
|
tools:targetApi="31"
|
||||||
android:icon="@mipmap/launcher_icon">
|
android:icon="@mipmap/launcher_icon">
|
||||||
|
@ -1,8 +1,15 @@
|
|||||||
package me.rhunk.snapenhance
|
package me.rhunk.snapenhance
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.AlertDialog
|
||||||
import android.content.Context
|
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.bridge.TranslationWrapper
|
||||||
import me.rhunk.snapenhance.download.DownloadTaskManager
|
import me.rhunk.snapenhance.download.DownloadTaskManager
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to store objects between activities and receivers
|
* Used to store objects between activities and receivers
|
||||||
@ -11,6 +18,24 @@ object SharedContext {
|
|||||||
lateinit var downloadTaskManager: DownloadTaskManager
|
lateinit var downloadTaskManager: DownloadTaskManager
|
||||||
lateinit var translation: TranslationWrapper
|
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) {
|
fun ensureInitialized(context: Context) {
|
||||||
if (!this::downloadTaskManager.isInitialized) {
|
if (!this::downloadTaskManager.isInitialized) {
|
||||||
downloadTaskManager = DownloadTaskManager().apply {
|
downloadTaskManager = DownloadTaskManager().apply {
|
||||||
@ -22,5 +47,29 @@ object SharedContext {
|
|||||||
loadFromContext(context)
|
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
|
package me.rhunk.snapenhance.download
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.media.MediaScannerConnection
|
import android.net.Uri
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.job
|
import kotlinx.coroutines.job
|
||||||
import kotlinx.coroutines.joinAll
|
import kotlinx.coroutines.joinAll
|
||||||
@ -16,6 +16,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import me.rhunk.snapenhance.Constants
|
import me.rhunk.snapenhance.Constants
|
||||||
import me.rhunk.snapenhance.Logger
|
import me.rhunk.snapenhance.Logger
|
||||||
|
import me.rhunk.snapenhance.SharedContext
|
||||||
import me.rhunk.snapenhance.data.FileType
|
import me.rhunk.snapenhance.data.FileType
|
||||||
import me.rhunk.snapenhance.download.data.DownloadRequest
|
import me.rhunk.snapenhance.download.data.DownloadRequest
|
||||||
import me.rhunk.snapenhance.download.data.InputMedia
|
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.data.PendingDownload
|
||||||
import me.rhunk.snapenhance.download.enums.DownloadMediaType
|
import me.rhunk.snapenhance.download.enums.DownloadMediaType
|
||||||
import me.rhunk.snapenhance.download.enums.DownloadStage
|
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.download.RemoteMediaResolver
|
||||||
|
import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
@ -113,6 +113,7 @@ class DownloadManagerReceiver : BroadcastReceiver() {
|
|||||||
return file
|
return file
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
||||||
private suspend fun saveMediaToGallery(inputFile: File, pendingDownload: PendingDownload) {
|
private suspend fun saveMediaToGallery(inputFile: File, pendingDownload: PendingDownload) {
|
||||||
if (coroutineContext.job.isCancelled) return
|
if (coroutineContext.job.isCancelled) return
|
||||||
|
|
||||||
@ -122,11 +123,24 @@ class DownloadManagerReceiver : BroadcastReceiver() {
|
|||||||
longToast(translation.format("failed_gallery_toast", "error" to "Unknown media type"))
|
longToast(translation.format("failed_gallery_toast", "error" to "Unknown media type"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val outputFile = File(pendingDownload.outputPath + "." + fileType.fileExtension).also { createNeededDirectories(it) }
|
val outputFile = File(pendingDownload.outputPath + "." + fileType.fileExtension).also { createNeededDirectories(it) }
|
||||||
|
|
||||||
inputFile.copyTo(outputFile, overwrite = true)
|
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
|
//print the path of the saved media
|
||||||
val parentName = outputFile.parentFile?.parentFile?.absolutePath?.let {
|
val parentName = outputFile.parentFile?.parentFile?.absolutePath?.let {
|
||||||
@ -136,9 +150,6 @@ class DownloadManagerReceiver : BroadcastReceiver() {
|
|||||||
shortToast(
|
shortToast(
|
||||||
translation.format("saved_toast", "path" to outputFile.absolutePath.replace(parentName ?: "", ""))
|
translation.format("saved_toast", "path" to outputFile.absolutePath.replace(parentName ?: "", ""))
|
||||||
)
|
)
|
||||||
|
|
||||||
pendingDownload.outputFile = outputFile.absolutePath
|
|
||||||
pendingDownload.downloadStage = DownloadStage.SAVED
|
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Logger.error(it)
|
Logger.error(it)
|
||||||
longToast(translation.format("failed_gallery_toast", "error" to it.toString()))
|
longToast(translation.format("failed_gallery_toast", "error" to it.toString()))
|
||||||
@ -254,10 +265,11 @@ class DownloadManagerReceiver : BroadcastReceiver() {
|
|||||||
return newFile
|
return newFile
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
if (intent.action != DOWNLOAD_ACTION) return
|
if (intent.action != DOWNLOAD_ACTION) return
|
||||||
this.context = context
|
this.context = context
|
||||||
|
Logger.debug("onReceive download")
|
||||||
|
|
||||||
SharedContext.ensureInitialized(context)
|
SharedContext.ensureInitialized(context)
|
||||||
|
|
||||||
val downloadRequest = DownloadRequest.fromBundle(intent.extras!!)
|
val downloadRequest = DownloadRequest.fromBundle(intent.extras!!)
|
||||||
@ -273,7 +285,7 @@ class DownloadManagerReceiver : BroadcastReceiver() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val pendingDownloadObject = PendingDownload.fromBundle(intent.extras!!)
|
val pendingDownloadObject = PendingDownload.fromBundle(intent.extras!!)
|
||||||
|
|
||||||
SharedContext.downloadTaskManager.addTask(pendingDownloadObject)
|
SharedContext.downloadTaskManager.addTask(pendingDownloadObject)
|
||||||
@ -287,6 +299,7 @@ class DownloadManagerReceiver : BroadcastReceiver() {
|
|||||||
val downloadedMedias = downloadInputMedias(downloadRequest).map {
|
val downloadedMedias = downloadInputMedias(downloadRequest).map {
|
||||||
it.key to DownloadedFile(it.value, FileType.fromFile(it.value))
|
it.key to DownloadedFile(it.value, FileType.fromFile(it.value))
|
||||||
}.toMap().toMutableMap()
|
}.toMap().toMutableMap()
|
||||||
|
Logger.debug("downloaded ${downloadedMedias.size} medias")
|
||||||
|
|
||||||
var shouldMergeOverlay = downloadRequest.shouldMergeOverlay
|
var shouldMergeOverlay = downloadRequest.shouldMergeOverlay
|
||||||
|
|
||||||
|
@ -152,7 +152,8 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
|
|||||||
if (uri.scheme == "file") {
|
if (uri.scheme == "file") {
|
||||||
return@let suspendCoroutine<String> { continuation ->
|
return@let suspendCoroutine<String> { continuation ->
|
||||||
context.downloadServer.ensureServerStarted {
|
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))
|
continuation.resumeWith(Result.success(url))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import android.app.Activity
|
|||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.Html
|
||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@ -12,6 +13,7 @@ import android.widget.ImageButton
|
|||||||
import android.widget.Switch
|
import android.widget.Switch
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import me.rhunk.snapenhance.BuildConfig
|
||||||
import me.rhunk.snapenhance.R
|
import me.rhunk.snapenhance.R
|
||||||
import me.rhunk.snapenhance.SharedContext
|
import me.rhunk.snapenhance.SharedContext
|
||||||
import me.rhunk.snapenhance.bridge.ConfigWrapper
|
import me.rhunk.snapenhance.bridge.ConfigWrapper
|
||||||
@ -104,6 +106,26 @@ class ConfigActivity : Activity() {
|
|||||||
|
|
||||||
val propertyListLayout = findViewById<ViewGroup>(R.id.property_list)
|
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
|
var currentCategory: ConfigCategory? = null
|
||||||
|
|
||||||
config.entries().forEach { (property, value) ->
|
config.entries().forEach { (property, value) ->
|
||||||
@ -248,5 +270,12 @@ class ConfigActivity : Activity() {
|
|||||||
propertyListLayout.addView(configItem)
|
propertyListLayout.addView(configItem)
|
||||||
addSeparator()
|
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.Constants
|
||||||
import me.rhunk.snapenhance.ui.config.ConfigActivity
|
import me.rhunk.snapenhance.ui.config.ConfigActivity
|
||||||
import me.rhunk.snapenhance.ui.menu.AbstractMenu
|
import me.rhunk.snapenhance.ui.menu.AbstractMenu
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("DiscouragedApi")
|
@SuppressLint("DiscouragedApi")
|
||||||
@ -49,6 +50,7 @@ class SettingsGearInjector : AbstractMenu() {
|
|||||||
val intent = Intent().apply {
|
val intent = Intent().apply {
|
||||||
setClassName(BuildConfig.APPLICATION_ID, ConfigActivity::class.java.name)
|
setClassName(BuildConfig.APPLICATION_ID, ConfigActivity::class.java.name)
|
||||||
}
|
}
|
||||||
|
intent.putExtra("lspatched", File(context.cacheDir, "lspatch/origin").exists())
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,22 +1,27 @@
|
|||||||
package me.rhunk.snapenhance.util.download
|
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
|
||||||
import me.rhunk.snapenhance.Logger.debug
|
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
import java.io.PrintWriter
|
import java.io.PrintWriter
|
||||||
import java.net.ServerSocket
|
import java.net.ServerSocket
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
|
import java.net.SocketTimeoutException
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.StringTokenizer
|
import java.util.StringTokenizer
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.ThreadLocalRandom
|
import java.util.concurrent.ThreadLocalRandom
|
||||||
|
|
||||||
class DownloadServer {
|
class DownloadServer(
|
||||||
|
private val timeout: Int = 10000
|
||||||
|
) {
|
||||||
private val port = ThreadLocalRandom.current().nextInt(10000, 65535)
|
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
|
private var serverSocket: ServerSocket? = null
|
||||||
|
|
||||||
fun ensureServerStarted(callback: DownloadServer.() -> Unit) {
|
fun ensureServerStarted(callback: DownloadServer.() -> Unit) {
|
||||||
@ -24,28 +29,37 @@ class DownloadServer {
|
|||||||
callback(this)
|
callback(this)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Thread {
|
|
||||||
try {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
debug("started web server on 127.0.0.1:$port")
|
Logger.debug("starting download server on port $port")
|
||||||
serverSocket = ServerSocket(port)
|
serverSocket = ServerSocket(port)
|
||||||
callback(this)
|
serverSocket!!.soTimeout = timeout
|
||||||
while (!serverSocket!!.isClosed) {
|
callback(this@DownloadServer)
|
||||||
try {
|
while (!serverSocket!!.isClosed) {
|
||||||
val socket = serverSocket!!.accept()
|
try {
|
||||||
Thread { handleRequest(socket) }.start()
|
val socket = serverSocket!!.accept()
|
||||||
} catch (e: Throwable) {
|
launch(Dispatchers.IO) {
|
||||||
Logger.xposedLog(e)
|
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)
|
val key = System.nanoTime().toString(16)
|
||||||
cachedData[key] = inputStream
|
cachedData[key] = inputStream to size
|
||||||
return "http://127.0.0.1:$port/$key"
|
return "http://127.0.0.1:$port/$key"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,14 +110,11 @@ class DownloadServer {
|
|||||||
with(writer) {
|
with(writer) {
|
||||||
println("HTTP/1.1 200 OK")
|
println("HTTP/1.1 200 OK")
|
||||||
println("Content-type: " + "application/octet-stream")
|
println("Content-type: " + "application/octet-stream")
|
||||||
|
println("Content-length: " + requestedData.second)
|
||||||
println()
|
println()
|
||||||
flush()
|
flush()
|
||||||
}
|
}
|
||||||
val buffer = ByteArray(1024)
|
requestedData.first.copyTo(outputStream)
|
||||||
var bytesRead: Int
|
|
||||||
while (requestedData.read(buffer).also { bytesRead = it } != -1) {
|
|
||||||
outputStream.write(buffer, 0, bytesRead)
|
|
||||||
}
|
|
||||||
outputStream.flush()
|
outputStream.flush()
|
||||||
cachedData.remove(fileRequested)
|
cachedData.remove(fileRequested)
|
||||||
close()
|
close()
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:background="@color/primaryBackground"
|
android:background="@color/primaryBackground"
|
||||||
android:layout_width="match_parent"
|
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