feat(android): bundle GeckoView and bump version to 1.0.2
This commit is contained in:
@@ -13,7 +13,7 @@ if (gradle.startParameter.taskNames.any { it.contains("release", ignoreCase = tr
|
||||
|
||||
android {
|
||||
namespace = "ren.misaka.dshapp"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
compileSdk { version = release(37) { minorApiLevel = 0 } }
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
@@ -26,7 +26,7 @@ android {
|
||||
applicationId = "ren.misaka.dshapp"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
minSdk = 26
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
// Uses the version code from pubspec.yaml. When using split APKs, 1000 * ABI_VERSION
|
||||
// is added automatically by Flutter. (https://developer.android.com/studio/build/configure-apk-splits#configure-APK-versions)
|
||||
@@ -73,3 +73,7 @@ kotlin {
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.mozilla.geckoview:geckoview:158.0.20261007201254")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
package ren.misaka.dshapp
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.Toast
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.StandardMessageCodec
|
||||
import io.flutter.plugin.platform.PlatformView
|
||||
import io.flutter.plugin.platform.PlatformViewFactory
|
||||
import org.mozilla.geckoview.*
|
||||
import org.mozilla.geckoview.GeckoSession.PromptDelegate.*
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
private object BrowserRuntime {
|
||||
private var instance: GeckoRuntime? = null
|
||||
fun get(context: Context): GeckoRuntime = instance ?: GeckoRuntime.create(context.applicationContext).also { instance = it }
|
||||
}
|
||||
|
||||
internal class GeckoViewFactory(private val activity: MainActivity, private val messenger: BinaryMessenger) :
|
||||
PlatformViewFactory(StandardMessageCodec.INSTANCE) {
|
||||
override fun create(context: Context, viewId: Int, args: Any?): PlatformView {
|
||||
val url = (args as? Map<*, *>)?.get("url") as? String
|
||||
val uri = url?.let { Uri.parse(it) }
|
||||
require(uri != null && uri.scheme == "https" && !uri.host.isNullOrBlank() && uri.userInfo == null) {
|
||||
"GeckoView requires an HTTPS url without credentials"
|
||||
}
|
||||
return GeckoPlatformView(activity, context, messenger, viewId, url!!)
|
||||
}
|
||||
}
|
||||
|
||||
internal class GeckoPlatformView(
|
||||
private val activity: MainActivity, context: Context, messenger: BinaryMessenger, id: Int,
|
||||
private val initialUrl: String
|
||||
) : PlatformView {
|
||||
private val view = GeckoView(context).apply { setViewBackend(GeckoView.BACKEND_TEXTURE_VIEW) }
|
||||
private val session = GeckoSession(GeckoSessionSettings.Builder().usePrivateMode(false).build())
|
||||
private val channel = MethodChannel(messenger, "dsh.workbench/gecko/" + id)
|
||||
private val prompts = mutableMapOf<BasePrompt, () -> Unit>()
|
||||
private val streams = java.util.Collections.synchronizedSet(mutableSetOf<InputStream>())
|
||||
private val io = Executors.newSingleThreadExecutor()
|
||||
@Volatile private var disposed = false
|
||||
private var loaded = false
|
||||
private var opened = false
|
||||
private var canGoBack = false
|
||||
private var loadFailed = false
|
||||
|
||||
init {
|
||||
session.progressDelegate = object : GeckoSession.ProgressDelegate {
|
||||
override fun onPageStart(session: GeckoSession, url: String) { loadFailed = false; emit("onPageStarted") }
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||
if (!loadFailed) emit("onPageFinished")
|
||||
}
|
||||
}
|
||||
session.navigationDelegate = object : GeckoSession.NavigationDelegate {
|
||||
override fun onCanGoBack(session: GeckoSession, value: Boolean) { canGoBack = value }
|
||||
override fun onLoadError(session: GeckoSession, uri: String?, error: WebRequestError): GeckoResult<String>? {
|
||||
loadFailed = true
|
||||
emit("onLoadError", "页面加载失败 (" + error.code + ")")
|
||||
return null
|
||||
}
|
||||
}
|
||||
session.contentDelegate = object : GeckoSession.ContentDelegate {
|
||||
override fun onExternalResponse(session: GeckoSession, response: WebResponse) { saveResponse(response) }
|
||||
override fun onCrash(session: GeckoSession) { processStopped() }
|
||||
override fun onKill(session: GeckoSession) { processStopped() }
|
||||
}
|
||||
session.promptDelegate = object : GeckoSession.PromptDelegate {
|
||||
override fun onAlertPrompt(session: GeckoSession, prompt: AlertPrompt) =
|
||||
dialog(prompt, prompt.message, positive = { prompt.dismiss() })
|
||||
override fun onButtonPrompt(session: GeckoSession, prompt: ButtonPrompt) =
|
||||
dialog(prompt, prompt.message, positive = { prompt.confirm(ButtonPrompt.Type.POSITIVE) }, negative = { prompt.confirm(ButtonPrompt.Type.NEGATIVE) })
|
||||
override fun onTextPrompt(session: GeckoSession, prompt: TextPrompt): GeckoResult<PromptResponse> {
|
||||
val input = EditText(activity).apply { setText(prompt.defaultValue) }
|
||||
return dialog(prompt, prompt.message, input, { prompt.confirm(input.text.toString()) }, { prompt.dismiss() })
|
||||
}
|
||||
override fun onFilePrompt(session: GeckoSession, prompt: FilePrompt) = chooseFiles(prompt)
|
||||
override fun onChoicePrompt(session: GeckoSession, prompt: ChoicePrompt) = chooseOptions(prompt)
|
||||
}
|
||||
activity.browserViews.add(this)
|
||||
channel.setMethodCallHandler { call, result ->
|
||||
try {
|
||||
when (call.method) {
|
||||
"load" -> { loadInitial(); result.success(null) }
|
||||
"goBack" -> { val back = canGoBack; if (back) session.goBack(); result.success(back) }
|
||||
"reload" -> { if (loaded) session.reload() else loadInitial(); result.success(null) }
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
} catch (error: Exception) { result.error("GECKO_ERROR", error.message, null) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun emit(method: String, value: String? = null) {
|
||||
activity.runOnUiThread { if (!disposed) channel.invokeMethod(method, value) }
|
||||
}
|
||||
|
||||
private fun loadInitial() {
|
||||
if (!opened) {
|
||||
session.open(BrowserRuntime.get(activity))
|
||||
opened = true
|
||||
view.setSession(session)
|
||||
setActive(activity.browserActive)
|
||||
}
|
||||
if (!loaded) { session.loadUri(initialUrl); loaded = true }
|
||||
}
|
||||
|
||||
private fun processStopped() {
|
||||
opened = false
|
||||
loaded = false
|
||||
canGoBack = false
|
||||
loadFailed = true
|
||||
view.releaseSession()
|
||||
activity.cancelDocuments(this)
|
||||
prompts.values.toList().forEach { it() }
|
||||
emit("onLoadError", "网页进程已停止,请重试")
|
||||
}
|
||||
|
||||
private fun chooseOptions(prompt: ChoicePrompt): GeckoResult<PromptResponse> {
|
||||
val result = GeckoResult<PromptResponse>()
|
||||
val choices = mutableListOf<ChoicePrompt.Choice>()
|
||||
fun flatten(items: Array<ChoicePrompt.Choice>) {
|
||||
items.filter { !it.disabled && !it.separator }.forEach { choice ->
|
||||
val children = choice.items
|
||||
if (children != null) flatten(children) else choices.add(choice)
|
||||
}
|
||||
}
|
||||
flatten(prompt.choices)
|
||||
var dialog: AlertDialog? = null
|
||||
fun finish(ids: Array<String>?) {
|
||||
if (!prompt.isComplete) result.complete(if (ids == null) prompt.dismiss() else prompt.confirm(ids))
|
||||
prompt.setDelegate(null)
|
||||
prompts.remove(prompt)
|
||||
dialog?.dismiss()
|
||||
}
|
||||
prompts[prompt] = { finish(null) }
|
||||
prompt.setDelegate(object : PromptInstanceDelegate {
|
||||
override fun onPromptDismiss(prompt: BasePrompt) { finish(null) }
|
||||
override fun onPromptUpdate(prompt: BasePrompt) { finish(null) }
|
||||
})
|
||||
val labels = choices.map { it.label }.toTypedArray()
|
||||
val builder = AlertDialog.Builder(activity).setTitle(prompt.title)
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> finish(null) }
|
||||
.setOnCancelListener { finish(null) }
|
||||
if (prompt.type == ChoicePrompt.Type.MULTIPLE) {
|
||||
val checked = choices.map { it.selected }.toBooleanArray()
|
||||
builder.setMultiChoiceItems(labels, checked) { _, index, selected -> checked[index] = selected }
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
finish(choices.filterIndexed { index, _ -> checked[index] }.map { it.id }.toTypedArray())
|
||||
}
|
||||
} else {
|
||||
builder.setSingleChoiceItems(labels, choices.indexOfFirst { it.selected }) { _, index ->
|
||||
finish(arrayOf(choices[index].id))
|
||||
}
|
||||
}
|
||||
dialog = builder.create()
|
||||
dialog.show()
|
||||
return result
|
||||
}
|
||||
|
||||
private fun dialog(
|
||||
prompt: BasePrompt, message: String?, input: EditText? = null,
|
||||
positive: () -> PromptResponse, negative: (() -> PromptResponse)? = null
|
||||
): GeckoResult<PromptResponse> {
|
||||
val result = GeckoResult<PromptResponse>()
|
||||
var dialog: AlertDialog? = null
|
||||
fun finish(response: () -> PromptResponse) {
|
||||
if (!prompt.isComplete) result.complete(response())
|
||||
prompts.remove(prompt)
|
||||
prompt.setDelegate(null)
|
||||
dialog?.dismiss()
|
||||
}
|
||||
val cancel = { finish { prompt.dismiss() } }
|
||||
prompts[prompt] = cancel
|
||||
prompt.setDelegate(object : PromptInstanceDelegate {
|
||||
override fun onPromptDismiss(prompt: BasePrompt) { cancel() }
|
||||
})
|
||||
val builder = AlertDialog.Builder(activity).setTitle(prompt.title).setMessage(message)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> finish(positive) }
|
||||
.setOnCancelListener { cancel() }
|
||||
if (input != null) builder.setView(input)
|
||||
if (negative != null) builder.setNegativeButton(android.R.string.cancel) { _, _ -> finish(negative) }
|
||||
dialog = builder.create()
|
||||
dialog.show()
|
||||
return result
|
||||
}
|
||||
|
||||
private fun chooseFiles(prompt: FilePrompt): GeckoResult<PromptResponse> {
|
||||
val result = GeckoResult<PromptResponse>()
|
||||
fun finish(uris: Array<Uri>?) {
|
||||
if (!prompt.isComplete) {
|
||||
try {
|
||||
result.complete(if (uris.isNullOrEmpty()) prompt.dismiss() else prompt.confirm(activity.applicationContext, uris))
|
||||
} catch (error: Exception) {
|
||||
if (!prompt.isComplete) result.complete(prompt.dismiss())
|
||||
Toast.makeText(activity, "无法读取所选文件:" + error.message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
prompts.remove(prompt)
|
||||
prompt.setDelegate(null)
|
||||
}
|
||||
prompts[prompt] = { finish(null) }
|
||||
prompt.setDelegate(object : PromptInstanceDelegate {
|
||||
override fun onPromptDismiss(prompt: BasePrompt) { finish(null) }
|
||||
})
|
||||
val folder = prompt.type == FilePrompt.Type.FOLDER
|
||||
val intent = Intent(if (folder) Intent.ACTION_OPEN_DOCUMENT_TREE else Intent.ACTION_OPEN_DOCUMENT)
|
||||
if (!folder) {
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
val types = prompt.mimeTypes?.filter { it.contains("/") }?.toTypedArray()
|
||||
intent.type = types?.singleOrNull() ?: "*/*"
|
||||
if (!types.isNullOrEmpty()) intent.putExtra(Intent.EXTRA_MIME_TYPES, types)
|
||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, prompt.type == FilePrompt.Type.MULTIPLE)
|
||||
}
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
try {
|
||||
activity.launchDocument(this, intent) { code, data ->
|
||||
if (code != Activity.RESULT_OK || data == null) finish(null)
|
||||
else {
|
||||
val uris = mutableListOf<Uri>()
|
||||
data.clipData?.let { clip -> for (i in 0 until clip.itemCount) uris.add(clip.getItemAt(i).uri) }
|
||||
if (uris.isEmpty()) data.data?.let { uris.add(it) }
|
||||
finish((if (prompt.type == FilePrompt.Type.MULTIPLE) uris else uris.take(1)).toTypedArray())
|
||||
}
|
||||
}
|
||||
} catch (error: Exception) { finish(null); Toast.makeText(activity, "无法打开文件选择器:" + error.message, Toast.LENGTH_LONG).show() }
|
||||
return result
|
||||
}
|
||||
|
||||
private fun saveResponse(response: WebResponse) {
|
||||
if (!loadFailed) emit("onPageFinished")
|
||||
val body = response.body
|
||||
if (body == null) { emit("onDownloadError", "下载响应没有内容"); return }
|
||||
if (disposed) { runCatching { body.close() }; return }
|
||||
streams.add(body)
|
||||
try {
|
||||
val uri = Uri.parse(response.uri)
|
||||
val disposition = response.headers.entries.firstOrNull { it.key.equals("content-disposition", true) }?.value
|
||||
val encodedName = disposition?.let { Regex("(?i)filename\\*\\s*=\\s*UTF-8''([^;]+)").find(it)?.groupValues?.get(1) }
|
||||
val headerName = encodedName?.let { Uri.decode(it) } ?: disposition?.let {
|
||||
Regex("(?i)filename\\s*=\\s*(?:\"([^\"]*)\"|([^;]*))").find(it)?.let { match ->
|
||||
match.groupValues[1].ifEmpty { match.groupValues[2].trim() }
|
||||
}
|
||||
}
|
||||
val name = (headerName ?: (if (uri.isHierarchical) uri.getQueryParameter("path")?.substringAfterLast('/') else null) ?: uri.lastPathSegment ?: "download")
|
||||
.map { if (it.isISOControl() || it in "\\/:*?\"<>|") '_' else it }.joinToString("")
|
||||
.take(120).ifBlank { "download" }
|
||||
val mime = response.headers.entries.firstOrNull { it.key.equals("content-type", true) }?.value?.substringBefore(';') ?: "application/octet-stream"
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).addCategory(Intent.CATEGORY_OPENABLE)
|
||||
.setType(mime).putExtra(Intent.EXTRA_TITLE, name)
|
||||
activity.launchDocument(this, intent) { code, data ->
|
||||
val destination = data?.data
|
||||
if (code != Activity.RESULT_OK || destination == null || disposed) {
|
||||
closeBody(body)
|
||||
} else {
|
||||
io.execute {
|
||||
try {
|
||||
check(!disposed) { "下载已取消" }
|
||||
activity.contentResolver.openOutputStream(destination, "w").use { output ->
|
||||
checkNotNull(output) { "无法打开目标文件" }
|
||||
body.use { it.copyTo(output, 64 * 1024) }
|
||||
}
|
||||
activity.runOnUiThread { if (!disposed) Toast.makeText(activity, "下载已保存", Toast.LENGTH_SHORT).show() }
|
||||
} catch (error: Exception) {
|
||||
runCatching { DocumentsContract.deleteDocument(activity.contentResolver, destination) }
|
||||
emit("onDownloadError", "保存下载失败:" + error.message)
|
||||
} finally { closeBody(body) }
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error: Exception) { closeBody(body); emit("onDownloadError", "无法保存下载:" + error.message) }
|
||||
}
|
||||
|
||||
private fun closeBody(body: InputStream) { streams.remove(body); runCatching { body.close() } }
|
||||
|
||||
internal fun setActive(active: Boolean) { if (!disposed && opened) { session.setActive(active); session.setFocused(active) } }
|
||||
override fun getView(): View = view
|
||||
override fun dispose() {
|
||||
if (disposed) return
|
||||
disposed = true
|
||||
channel.setMethodCallHandler(null)
|
||||
activity.cancelDocuments(this)
|
||||
prompts.values.toList().forEach { it() }
|
||||
synchronized(streams) { streams.toList() }.forEach { closeBody(it) }
|
||||
io.shutdown()
|
||||
if (opened) {
|
||||
session.setActive(false)
|
||||
view.releaseSession()
|
||||
session.close()
|
||||
}
|
||||
session.progressDelegate = null
|
||||
session.navigationDelegate = null
|
||||
session.contentDelegate = null
|
||||
session.promptDelegate = null
|
||||
activity.browserViews.remove(this)
|
||||
}
|
||||
}
|
||||
@@ -1,92 +1,62 @@
|
||||
package ren.misaka.dshapp
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.webkit.CookieManager
|
||||
import android.widget.Toast
|
||||
import android.content.Intent
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
|
||||
companion object {
|
||||
private const val CHANNEL = "dsh.workbench/downloads"
|
||||
}
|
||||
internal val browserViews = mutableSetOf<GeckoPlatformView>()
|
||||
private val requests = mutableMapOf<Int, Pair<GeckoPlatformView, (Int, Intent?) -> Unit>>()
|
||||
private var nextRequest = 2000
|
||||
internal var browserActive = false
|
||||
private set
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
|
||||
.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"download" -> {
|
||||
val url = call.argument<String>("url")
|
||||
if (url == null) {
|
||||
result.error("ARG_MISSING", "url is required", null)
|
||||
return@setMethodCallHandler
|
||||
}
|
||||
try {
|
||||
startDownload(url)
|
||||
result.success(true)
|
||||
} catch (e: Exception) {
|
||||
result.error("DOWNLOAD_FAILED", e.message, null)
|
||||
}
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
flutterEngine.platformViewsController.registry.registerViewFactory(
|
||||
"dsh.workbench/gecko", GeckoViewFactory(this, flutterEngine.dartExecutor.binaryMessenger)
|
||||
)
|
||||
}
|
||||
|
||||
/// 把下载交给系统 DownloadManager,并携带面板登录 Cookie
|
||||
/// (下载接口与站点同源、需要 JWT 会话,不能走无 Cookie 的外部浏览器)。
|
||||
private fun startDownload(url: String) {
|
||||
val request = DownloadManager.Request(Uri.parse(url))
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
.setAllowedOverMetered(true)
|
||||
.setAllowedOverRoaming(true)
|
||||
|
||||
CookieManager.getInstance().getCookie(url)?.takeIf { it.isNotEmpty() }?.let { cookie ->
|
||||
request.addRequestHeader("Cookie", cookie)
|
||||
internal fun launchDocument(owner: GeckoPlatformView, intent: Intent, callback: (Int, Intent?) -> Unit) {
|
||||
val code = nextRequest++
|
||||
check(code <= 65535) { "Too many document requests" }
|
||||
requests[code] = owner to callback
|
||||
try {
|
||||
startActivityForResult(intent, code)
|
||||
} catch (error: Exception) {
|
||||
requests.remove(code)
|
||||
throw error
|
||||
}
|
||||
|
||||
val fileName = deriveFileName(url)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// DownloadManager 不接受 MediaStore 的 content:// URI。
|
||||
// targetSdk >= 29 时,系统下载器写公共 Downloads 不需要存储权限。
|
||||
request.setDestinationInExternalPublicDir(
|
||||
Environment.DIRECTORY_DOWNLOADS, fileName
|
||||
)
|
||||
} else {
|
||||
// Android 9-:公共目录写入需运行时权限;退到应用外部目录(免权限)
|
||||
@Suppress("DEPRECATION")
|
||||
request.setDestinationInExternalFilesDir(
|
||||
this, Environment.DIRECTORY_DOWNLOADS, fileName
|
||||
)
|
||||
}
|
||||
|
||||
val manager = getSystemService(DownloadManager::class.java)
|
||||
manager.enqueue(request)
|
||||
val destination = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
"系统下载目录"
|
||||
} else {
|
||||
"应用下载目录"
|
||||
}
|
||||
Toast.makeText(this, "已开始下载到$destination:$fileName", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
/// 从下载 URL 推断文件名,优先取 path 参数的 basename(即真实文件名)。
|
||||
private fun deriveFileName(url: String): String {
|
||||
val uri = Uri.parse(url)
|
||||
uri.getQueryParameter("path")?.substringAfterLast('/')?.takeIf { it.isNotBlank() }
|
||||
?.let { return sanitize(it) }
|
||||
uri.lastPathSegment?.takeIf { it.isNotBlank() && it != "download" }
|
||||
?.let { return sanitize(it) }
|
||||
return "dsh-download-${System.currentTimeMillis()}"
|
||||
internal fun cancelDocuments(owner: GeckoPlatformView) {
|
||||
requests.entries.filter { it.value.first === owner }.map { it.key }.forEach { code ->
|
||||
requests.remove(code)?.second?.invoke(RESULT_CANCELED, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sanitize(name: String): String =
|
||||
name.replace(Regex("[\\\\/:*?\"<>|]"), "_").take(120).ifBlank { "file" }
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
val request = requests.remove(requestCode)
|
||||
if (request != null) request.second(resultCode, data)
|
||||
else super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
browserActive = true
|
||||
browserViews.forEach { it.setActive(true) }
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
browserActive = false
|
||||
browserViews.forEach { it.setActive(false) }
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
browserViews.toList().forEach { it.dispose() }
|
||||
requests.clear()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user