Фон
Я пытаюсь загрузить какой-то URL в фоновом режиме, но таким же образом WebView загружает его в Activity.
Есть несколько причин, по которым разработчики хотели бы этого (и просили об этом здесь ), например, запуск JavaScript без Activity, кэширование, мониторинг изменений веб-сайтов, удаление ...
Проблема
Похоже, что на некоторых устройствах и версиях Android (например, Pixel 2 с Android P) это прекрасно работает на Worker , но на некоторых других (возможно, на старых версиях) Android), я могу сделать это хорошо и безопасно только на сервисе переднего плана с видом сверху, используя разрешение SYSTEM_ALERT_WINDOW.
Дело в том, что нам нужно использовать его в фоновом режиме, поскольку у нас уже есть работник, предназначенный для других целей. Мы предпочли бы не добавлять службу переднего плана только для этого, так как это усложнит ситуацию, добавит необходимые разрешения и сделает уведомление для пользователя, если ему необходимо выполнить работу.
Что я пробовал и нашел
Выполняя поиск в Интернете, я могу найти только несколько упоминаний об этом сценарии ( здесь и здесь ). Основное решение, действительно, заключается в том, чтобы иметь сервис переднего плана с видом сверху.
Чтобы проверить, нормально ли загружается сайт, я добавил журналы в различных обратных вызовах, в том числе onProgressChanged, onConsoleMessage, onReceivedError, onPageFinished, shouldInterceptRequest, onPageStarted. Вся часть WebViewClient и WebChromeClient классов.
Я тестировал на веб-сайтах, которые, как я знаю, должны записывать в консоль, немного сложнее и требуют некоторого времени для загрузки, такие как Reddit и Имгур .
Важно разрешить JavaScript, так как нам может понадобиться его использовать, и веб-сайты загружаются, как и должно, когда он включен, поэтому я установил javaScriptEnabled=true
. Я заметил, что есть также javaScriptCanOpenWindowsAutomatically
, но, как я прочитал, это обычно не нужно, поэтому я на самом деле не использовал его. Кроме того, кажется, что включение этого параметра приводит к тому, что мои решения (на Worker) перестают работать, но, возможно, это просто совпадение. Также важно знать, что WebView следует использовать в потоке пользовательского интерфейса, поэтому я поместил его обработку в обработчик, связанный с потоком пользовательского интерфейса.
Я попытался включить больше флагов в WebSettings классе WebView, и я также попытался эмулировать, что он находится внутри контейнера, измеряя его.
Попытался немного задержать загрузку и попытался сначала загрузить пустой URL. В некоторых случаях это, кажется, помогло, но это не соответствует.
Не похоже, что что-то помогло, но в некоторых случайных случаях разные решения все же работали (но не согласовывались).
Вот мой текущий код, который также включает кое-что из того, что я пробовал (проект доступен здесь ):
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
...
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>
Вопросы
Основной вопрос: возможно ли вообще использовать WebView в Worker?
Почему на Android P в Worker все работает нормально, а на других нет?
Почему иногда это работало на Рабочем?
Есть ли альтернатива, либо сделать это в Worker, либо иметь альтернативу WebView, которая может выполнять те же операции загрузки веб-страниц и запуска на них Javascripts?