7

Background

I'm trying to load some URL in the background, but in the same way WebView loads it in an Activity.

There are multiple reasons developers would want it (and requested about it here) , such as running JavaScript without Activity, caching, monitor websites changes, scrapping ...

The problem

It seems that on some devices and Android versions (Pixel 2 with Android P, for example), this works fine on Worker , but on some others (probably on older versions of Android), I can do it well and safely only on a foreground service with on-top view using the SYSTEM_ALERT_WINDOW permission.

Thing is, we need to use it in the background, as we have a Worker already that is intended for other things. We would prefer not to add a foreground service just for that, as it would make things complex, add a required permission, and would make a notification for the user as long as it needs to do the work.

What I've tried&found

  1. Searching the Internet, I can find only few mention this scenario (here and here). The main solution is indeed to have a foreground service with on-top view.

  2. In order to check if the website loads fine, I've added logs in various callbacks, including onProgressChanged , onConsoleMessage, onReceivedError , onPageFinished , shouldInterceptRequest, onPageStarted . All part of WebViewClient and WebChromeClient classes.

I've tested on websites that I know should write to the console, a bit complex and take some time to load, such as Reddit and Imgur .

  1. It is important to let JavaScript enabled, as we might need to use it, and websites load as they should when it's enabled, so I've set javaScriptEnabled=true . I've noticed there is also javaScriptCanOpenWindowsAutomatically , but as I've read this isn't usually needed, so I didn't really use it. Plus it seems that enabling it causes my solutions (on Worker) to fail more, but maybe it's just a coincidence . Also, it's important to know that WebView should be used on the UI thread, so I've put its handling on a Handler that is associated with the UI thread.

  2. I've tried to enable more flags in WebSettings class of the WebView, and I also tried to emulate that it's inside of a container, by measuring it.

  3. Tried to delay the loading a bit, and tried to load an empty URL first. On some cases it seemed to help, but it's not consistent .

Doesn't seem like anything helped, but on some random cases various solutions seemed to work nevertheless (but not consistent).

Here's my current code, which also includes some of what I've tried (project available 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)
    }

}

MainActivity.kt

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 file

...
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

<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>

The questions

  1. Main question: Is it even possible to use a WebView within Worker?

  2. How come it seems to work fine on Android P in a Worker, but not on others?

  3. How come sometimes it did work on a Worker?

  4. Is there an alternative, either to do it in Worker, or having an alternative to WebView that is capable of the same operations of loading webpages and running Javascripts on them ?

android developer
  • 114,585
  • 152
  • 739
  • 1,270
  • maybe these below link can help you – Ashwini Saini Sep 04 '18 at 08:54
  • https://stackoverflow.com/questions/19278074/webview-loading-page-android – Ashwini Saini Sep 04 '18 at 08:54
  • https://stackoverflow.com/questions/11231666/is-it-possible-to-separate-webviews-ui-and-http-threads – Ashwini Saini Sep 04 '18 at 08:54
  • https://stackoverflow.com/questions/8353840/a-webview-in-a-thread-cant-be-created – Ashwini Saini Sep 04 '18 at 08:54
  • @AshwiniViolet I'm well aware that I should make it work on the UI thread, and that's also what I did on the code. I will update my points to make sure you understand. That's not what my question is about. – android developer Sep 04 '18 at 08:56
  • loadUrl method of webview is already Asynchronous so it does not block main thread, so i think there is no need to use webview in worker thread. – tejendra singh Sep 04 '18 at 09:28
  • 1
    @tejendrasingh That's not what the question is about. WebView seems to require to exist inside Activity or inside Foreground service with it on-top. Trying to load WebView inside Worker doesn't seem to work on some cases. – android developer Sep 04 '18 at 10:12
  • @androiddeveloper This problem is solved? I want to do webview like headless Selnium. But [android API 26](https://developer.android.com/about/versions/oreo/background) restrict background service. Is there any idea how to do it? – 4rigener Oct 29 '21 at 05:38
  • @4rigener I don't know how to solve it. Probably need to use your own engine, which I have no idea how to do. – android developer Oct 31 '21 at 08:55
  • @androiddeveloper What's mean engine? I need to create my own webview class like android OS developer create webview class? – 4rigener Nov 02 '21 at 01:04
  • @4rigener Again, I don't know. By "engine" I mean the entire loading and rendering. – android developer Nov 02 '21 at 01:07

1 Answers1

1

I think we need another tool for these kind of scenarios. My honest opinion is, it's a WebView, a view after all, which is designed to display web pages. I know as we need to implement hacky solutions to resolve such cases, but I believe these are not webView concerns either.

What I think would be the solution is, instead of observing web page and listening javaScripts for changes, changes should be delivered to app by a proper message ( push / socket / web service ).

If it's not possible to do it this way, I believe request (https://issuetracker.google.com/issues/113346931) should not be "being able to run WebView in a service" but a proper addition to SDK which would perform operations you mentioned.

Mel
  • 1,730
  • 17
  • 33
  • When you write "by a proper message" you mean a server, outside of the device ? – android developer Sep 04 '18 at 10:13
  • Yes, a message from server side, delivered by any other channel to the device but as JS messages. – Mel Sep 04 '18 at 11:02
  • That's not always an option, and it could cause a lot of work on the server, the more users there are and the more work-per-user there is. – android developer Sep 04 '18 at 14:48
  • That's why I said if it's possible. I think that kind of work not supposed to be on frontend since it'll consume battery & network data, and may cost the app losing users due to consumption. Calculations should be done carefully. – Mel Sep 05 '18 at 11:20
  • True, but this is true for all operations that are done on a Worker... It even has conditions that it will get triggered only when Internet is available, and you can also set it to trigger only when the phone is connected to a charger. – android developer Sep 06 '18 at 09:52
  • Well, if you look at it that way it's true for all operations done out of a Worker too, but that's not what I mention above by "that kind of work". I'm referring the weight of a casual I/O call vs initializing a webView and listening events. – Mel Sep 06 '18 at 10:13
  • What's the difference? Both do long time operations. The problem with WebView is that you really can't have any clue of what's going on there, as things might load using JavaScript. But an analogue to this could be reaching out to the server, and not getting a response of the result for a long time. – android developer Sep 06 '18 at 10:20
  • Difference will be in usage of memory, battery, network and CPU so it's performance-wise, not functionality. – Mel Sep 06 '18 at 10:28
  • Of course. I think that sadly the only solution for this question is indeed an on-top view on a foreground service. Using a worker somehow worked for me on Android P. Not sure how it's possible. – android developer Sep 06 '18 at 11:05
  • Exactly, I really think they can add support for such cases in SDK, so we don't use webView for this kind of functionality. – Mel Sep 06 '18 at 11:43