是否可以在Worker中使用WebView?

问题描述 投票:2回答:1

背景

我正在尝试在后台加载一些URL,但与WebView在Activity中加载它的方式相同。

开发人员想要它的原因有很多(并且要求here),例如运行没有Activity的JavaScript,缓存,监控网站更改,报废......

问题

似乎在某些设备和Android版本(例如Pixel 2与Android P)上,这在Worker上工作正常,但在其他一些(可能在旧版本的Android上),我只能在前景上做得很好而且安全使用SYSTEM_ALERT_WINDOW权限在顶部视图上进行服务。

事实上,我们需要在后台使用它,因为我们已经有了一个专门用于其他事情的Worker。我们宁愿不为此添加前台服务,因为它会使事情变得复杂,添加所需的权限,并且只要需要完成工作就会为用户发出通知。

我尝试过并发现了什么

  1. 在互联网上搜索,我发现只有少数人提到这种情况(herehere)。主要解决方案确实是具有顶部视图的前台服务。
  2. 为了检查网站是否正常加载,我在各种回调中添加了日志,包括onProgressChanged,onConsoleMessage,onReceivedError,onPageFinished,shouldInterceptRequest,onPageStarted。 WebViewClientWebChromeClient课程的所有部分。

我已经在我知道应该写入控制台的网站上进行了测试,有点复杂并需要一些时间来加载,例如RedditImgur

  1. 启用JavaScript非常重要,因为我们可能需要使用它,并且网站在启用时会加载,因此我设置了javaScriptEnabled=true。我注意到还有javaScriptCanOpenWindowsAutomatically,但正如我所读,这通常不需要,所以我没有真正使用它。此外,它似乎启用它会导致我的解决方案(在工人上)失败更多,但也许这只是一个巧合。此外,重要的是要知道应该在UI线程上使用WebView,因此我将其处理放在与UI线程关联的Handler上。
  2. 我试图在WebView的WebSettings类中启用更多标志,我还尝试通过测量来模拟它在容器内部。
  3. 试图延迟加载一点,并尝试首先加载一个空URL。在某些情况下它似乎有所帮助,但它并不一致。

似乎没有任何帮助,但在一些随机的情况下,各种解决方案似乎仍然有效(但不一致)。

这是我目前的代码,其中还包括我尝试过的一些内容(项目可用here):

Util.kt

object Util {
    @SuppressLint("SetJavaScriptEnabled")
    @UiThread
    fun getNewWebView(context: Context): WebView {
        val webView = WebView(context)
//        val screenWidth = context.resources.displayMetrics.widthPixels
//        val screenHeight = context.resources.displayMetrics.heightPixels
//        webView.measure(screenWidth, screenHeight)
//        webView.layout(0, 0, screenWidth, screenHeight)
//        webView.measure(600, 400);
//        webView.layout(0, 0, 600, 400);
        val webSettings = webView.settings
        webSettings.javaScriptEnabled = true
//        webSettings.loadWithOverviewMode = true
//        webSettings.useWideViewPort = true
//        webSettings.javaScriptCanOpenWindowsAutomatically = true
//        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
//            webSettings.allowFileAccessFromFileURLs = true
//            webSettings.allowUniversalAccessFromFileURLs = true
//        }
        webView.webChromeClient = object : WebChromeClient() {
            override fun onProgressChanged(view: WebView?, newProgress: Int) {
                super.onProgressChanged(view, newProgress)
                Log.d("appLog", "onProgressChanged:$newProgress " + view?.url)
            }

            override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
                if (consoleMessage != null)
                    Log.d("appLog", "webViewConsole:" + consoleMessage.message())
                return super.onConsoleMessage(consoleMessage)
            }
        }
        webView.webViewClient = object : WebViewClient() {

            override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceError) {
                Log.d("appLog", "error $request  $error")
            }

            override fun onPageFinished(view: WebView?, url: String?) {
                super.onPageFinished(view, url)
                Log.d("appLog", "onPageFinished:$url")
            }

            override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
                    Log.d("appLog", "shouldInterceptRequest:${request.url}")
                else
                    Log.d("appLog", "shouldInterceptRequest")
                return super.shouldInterceptRequest(view, request)
            }

            override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
                super.onPageStarted(view, url, favicon)
                Log.d("appLog", "onPageStarted:$url hasFavIcon?${favicon != null}")
            }

        }
        return webView
    }


    @TargetApi(Build.VERSION_CODES.M)
    fun isSystemAlertPermissionGranted(@NonNull context: Context): Boolean {
        return Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1 || Settings.canDrawOverlays(context)
    }

    fun requestSystemAlertPermission(context: Activity?, fragment: Fragment?, requestCode: Int) {
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1)
            return
        //http://developer.android.com/reference/android/Manifest.permission.html#SYSTEM_ALERT_WINDOW
        val packageName = if (context == null) fragment!!.activity!!.packageName else context.packageName
        var intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName"))
        try {
            if (fragment != null)
                fragment.startActivityForResult(intent, requestCode)
            else
                context!!.startActivityForResult(intent, requestCode)
        } catch (e: Exception) {
            intent = Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS)
            if (fragment != null)
                fragment.startActivityForResult(intent, requestCode)
            else
                context!!.startActivityForResult(intent, requestCode)
        }
    }

    /**
     * requests (if needed) system alert permission. returns true iff requested.
     * WARNING: You should always consider checking the result of this function
     */
    fun requestSystemAlertPermissionIfNeeded(activity: Activity?, fragment: Fragment?, requestCode: Int): Boolean {
        val context = activity ?: fragment!!.activity
        if (isSystemAlertPermissionGranted(context!!))
            return false
        requestSystemAlertPermission(activity, fragment, requestCode)
        return true
    }
}

MyService.kt

class MyService : Service() {

    override fun onBind(intent: Intent): IBinder? = null
    override fun onCreate() {
        super.onCreate()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            run {
                //general
                val channel = NotificationChannel("channel_id__general", "channel_name__general", NotificationManager.IMPORTANCE_DEFAULT)
                channel.enableLights(false)
                channel.setSound(null, null)
                notificationManager.createNotificationChannel(channel)
            }
        }
        val builder = NotificationCompat.Builder(this, "channel_id__general")
        builder.setSmallIcon(android.R.drawable.sym_def_app_icon).setContentTitle(getString(R.string.app_name))
        startForeground(1, builder.build())
    }

    @SuppressLint("SetJavaScriptEnabled")
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
        val params = WindowManager.LayoutParams(
                android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
                android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY else WindowManager.LayoutParams.TYPE_PHONE,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
                PixelFormat.TRANSLUCENT
        )
        params.gravity = Gravity.TOP or Gravity.START
        params.x = 0
        params.y = 0
        params.width = 0
        params.height = 0
        val webView = Util.getNewWebView(this)
//        webView.loadUrl("https://www.google.com/")
//        webView.loadUrl("https://www.google.com/")
//        webView.loadUrl("")
//        Handler().postDelayed( {
//        webView.loadUrl("")
        webView.loadUrl("https://imgur.com/a/GPlx4?desktop=1")
//        },5000L)
//        webView.loadUrl("https://imgur.com/a/GPlx4?desktop=1")
        windowManager.addView(webView, params)
        return super.onStartCommand(intent, flags, startId)
    }

}

我激活了。

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        startServiceButton.setOnClickListener {
            if (!Util.requestSystemAlertPermissionIfNeeded(this, null, REQUEST_DRAW_ON_TOP))
                ContextCompat.startForegroundService(this@MainActivity, Intent(this@MainActivity, MyService::class.java))
        }
        startWorkerButton.setOnClickListener {
            val workManager = WorkManager.getInstance()
            workManager.cancelAllWorkByTag(WORK_TAG)
            val builder = OneTimeWorkRequest.Builder(BackgroundWorker::class.java).addTag(WORK_TAG)
            builder.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED)
                    .setRequiresCharging(false).build())
            builder.setInitialDelay(5, TimeUnit.SECONDS)
            workManager.enqueue(builder.build())
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == REQUEST_DRAW_ON_TOP && Util.isSystemAlertPermissionGranted(this))
            ContextCompat.startForegroundService(this@MainActivity, Intent(this@MainActivity, MyService::class.java))
    }


    class BackgroundWorker : Worker() {
        val handler = Handler(Looper.getMainLooper())
        override fun doWork(): Result {
            Log.d("appLog", "doWork started")
            handler.post {
                val webView = Util.getNewWebView(applicationContext)
//        webView.loadUrl("https://www.google.com/")
        webView.loadUrl("https://www.google.com/")
//                webView.loadUrl("")
//                Handler().postDelayed({
//                    //                webView.loadUrl("")
////                    webView.loadUrl("https://imgur.com/a/GPlx4?desktop=1")
//                    webView.loadUrl("https://www.reddit.com/")
//
//                }, 1000L)
//        webView.loadUrl("https://imgur.com/a/GPlx4?desktop=1")
            }
            Thread.sleep(20000L)
            Log.d("appLog", "doWork finished")
            return Worker.Result.SUCCESS
        }
    }

    companion object {
        const val REQUEST_DRAW_ON_TOP = 1
        const val WORK_TAG = "WORK_TAG"
    }
}

activity_main.xml中

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center"
    android:orientation="vertical" tools:context=".MainActivity">

    <Button
        android:id="@+id/startServiceButton" android:layout_width="wrap_content" android:layout_height="wrap_content"
        android:text="start service"/>


    <Button
        android:id="@+id/startWorkerButton" android:layout_width="wrap_content" android:layout_height="wrap_content"
        android:text="start worker"/>
</LinearLayout>

gradle文件

...
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.0.0-rc02'
    implementation 'androidx.core:core-ktx:1.0.0-rc02'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.2'
    def work_version = "1.0.0-alpha08"
    implementation "android.arch.work:work-runtime-ktx:$work_version"
    implementation "android.arch.work:work-firebase:$work_version"
}

表现

<manifest package="com.example.webviewinbackgroundtest" xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

    <application
        android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"
        tools:ignore="AllowBackup,GoogleAppIndexingWarning">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

        <service
            android:name=".MyService" android:enabled="true" android:exported="true"/>
    </application>

</manifest>

问题

  1. 主要问题:甚至可以在Worker中使用WebView吗?
  2. 为什么它似乎在一个工人的Android P上正常工作,但在其他人上却没有?
  3. 为什么它有时会对工人起作用?
  4. 是否有替代方法,要么在Worker中执行,要么替代WebView,它能够执行相同的加载网页和在其上运行Javascripts的操作?
android webview android-service android-workmanager
1个回答
0
投票

我认为我们需要这种场景的另一种工具。我的诚实的意见是,这是一个WebView,毕竟是一个视图,旨在显示网页。我知道,因为我们需要实施hacky解决方案来解决这些问题,但我相信这些也不是webView的关注点。

我认为解决方案是,不是通过观察网页和监听javaScripts进行更改,而是应该通过正确的消息(推送/套接字/ Web服务)将更改传递给应用程序。

如果不可能这样做,我相信请求(https://issuetracker.google.com/issues/113346931)不应该“能够在服务中运行WebView”,而是SDK的适当添加,它将执行您提到的操作。

© www.soinside.com 2019 - 2024. All rights reserved.