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
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.
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 .
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.
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.
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
Main question: Is it even possible to use a WebView within Worker?
How come it seems to work fine on Android P in a Worker, but not on others?
How come sometimes it did work on a Worker?
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 ?
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.
Related
I am making a library that can be used to incorporate breaking and entering detection into any application
There is an arduino set to the alarm of the house which sends an SMS to a specific phone upon trigger
Within my sdk I register an sms receiver which upon receiving an sms with a specific text, should show a full screen activity (on top of the lockscreen too) that will alert the user
I created an application to test this behaviour
the application's package is : com.example.demo
the library's package is : com.example.sdk
the sms receiver looks like this:
class SMSReceiver : BroadcastReceiver() {
companion object {
const val TAG = "SMSReceiver"
}
private val logger by lazy { Injections.logger }
override fun onReceive(context: Context?, intent: Intent?) {
logger.log(TAG) { "Got sms" }
val ctx = context ?: return
val bundle = intent?.extras ?: return
val format = bundle.getString("format") ?: return
val pdus = (bundle["pdus"] as? Array<*>) ?: return
for (idx in pdus.indices) {
val pdu = pdus[idx] as? ByteArray ?: continue
val msg = SmsMessage.createFromPdu(pdu, format)
if (msg.messageBody.startsWith("theft event", true)) {
logger.log(TAG) { "Got theft event" }
abortBroadcast()
showTheftActivity(ctx, msg.messageBody)
break
}
}
}
private fun showTheftActivity(context: Context, messageBody: String) {
val intent = Intent(context, TheftActivity::class.java)
intent.addFlags(Intent.FLAG_FROM_BACKGROUND)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
// .addCategory(Intent.CATEGORY_LAUNCHER)
val location = messageBody.split(" ").getOrNull(2)
if (location != null) {
val coords = location.split(",")
if (coords.size == 2) {
val x = coords[0].toBigDecimalOrNull()
val y = coords[1].toBigDecimalOrNull()
if (x != null && y != null) {
intent.putExtra(TheftActivity.X, x.toString())
intent.putExtra(TheftActivity.Y, y.toString())
}
}
}
context.startActivity(intent)
}
}
the activity that should show on top of everything is this :
class TheftActivity : Activity() {
companion object {
const val X = "locationX"
const val Y = "locationY"
}
private val x: String? by lazy { intent.getStringExtra(X) }
private val y: String? by lazy { intent.getStringExtra(Y) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_theft)
val location = findViewById<Button>(R.id.locate)
if (x != null && y != null) {
location.setOnClickListener { Toast.makeText(applicationContext, "Going to $x , $y", Toast.LENGTH_SHORT).show() }
location.isEnabled = true
finish()
} else {
location.isEnabled = false
}
findViewById<Button>(R.id.cancel).setOnClickListener {
finish()
}
turnScreenOnAndKeyguardOff()
}
private fun turnScreenOnAndKeyguardOff() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(true)
setTurnScreenOn(true)
} else {
window.addFlags(
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
)
}
with(getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
requestDismissKeyguard(this#TheftActivity, null)
}
}
}
override fun onDestroy() {
super.onDestroy()
turnScreenOffAndKeyguardOn()
}
private fun turnScreenOffAndKeyguardOn() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(false)
setTurnScreenOn(false)
} else {
window.clearFlags(
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
)
}
}
}
and the sdk's android manifest contains this:
<application>
<activity
android:name=".ui.TheftActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:excludeFromRecents="true"
android:label="#string/title_activity_theft"
android:showOnLockScreen="true"
android:theme="#style/Theme.Sdk.Fullscreen" />
<receiver
android:name=".receivers.SMSReceiver"
android:enabled="true"
android:exported="true"
android:permission="android.permission.BROADCAST_SMS">
<intent-filter>
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
</application>
when testing this on an emulator I send the sms to trigger the theft event
if the activity for testing (the one in the com.example.demo package) is not closed then it is brought to the front , but if it is closed nothing happens (though I do see the log messages from the receiver)
how can I make my sms receiver open the TheftActivity instead of the main activity from the main package?
edit: if it helps, the theft activity seems to start and then get immediately destroyed
It looks like the system can't bring the activity to the foreground due to the restrictions implemented in Android Q
With Android Q, it is impossible to start an activity from the background automatically if your app does not include those exceptions listed in the link below.
https://developer.android.com/guide/components/activities/background-starts
For possible solutions :
https://stackoverflow.com/a/59421118/11982611
I need to get the name of any Android app when it opens and launch a password window. I have searched everywhere how to find out the name of the app which the user has opened but I have found no working solution yet. I have a service which I'm running on the background, but it only returns my app's name or the home screen name, no matter which app I open.
Here is my service code:
package com.example.applock
import android.app.ActivityManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.IBinder
import android.provider.Settings
class BackAppListenerService : Service() {
private var isRunning = false
private var lastApp = ""
override fun onCreate() {
println(TAG + "Service onCreate")
isRunning = true
val intent = Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
println(TAG + "Service onStartCommand")
//Creating new thread for my service
//Always write your long running tasks in a separate thread, to avoid ANR
Thread(Runnable { //Your logic that service will perform will be placed here
//In this example we are just looping and waits for 1000 milliseconds in each loop.
while (true) {
try {
Thread.sleep(1000)
} catch (e: Exception) {
}
val mActivityManager = this.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val mPackageName = mActivityManager.runningAppProcesses[0].processName
println(mPackageName)
}
}).start()
return START_STICKY
}
override fun onBind(arg0: Intent): IBinder? {
println(TAG + "Service onBind")
return null
}
override fun onDestroy() {
isRunning = false
println(TAG + "Service onDestroy")
}
companion object {
private const val TAG = "HelloService"
}
}
Ok, so I have figured it out.
First add this to your manifest.xml under the manifest tag:
<uses-permission
android:name="android.permission.PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions"/>
Then go to apps permissions > Usage Stats go to your app and enable it. You can also make a pop up asking for the user to do it once the apps loads.
Now, to get name of the current foreground app:
fun getForegroundApp(): String {
var currentApp = "NULL"
// You can delete the if-else statement if you don't care about Android versions
// lower than 5.0. Just keep the code that is inside the if and delete the one
// inside the else statement.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val usm = this.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager
val time = System.currentTimeMillis()
val appList =
usm.queryUsageStats(UsageStatsManager.INTERVAL_DAILY, time - 1000 * 1000, time)
if (appList != null && appList.size > 0) {
val mySortedMap: SortedMap<Long, UsageStats> =
TreeMap<Long, UsageStats>()
for (usageStats in appList) {
mySortedMap.put(usageStats.lastTimeUsed, usageStats)
}
if (mySortedMap != null && !mySortedMap.isEmpty()) {
currentApp = mySortedMap.get(mySortedMap.lastKey())!!.getPackageName()
}
}
} else {
val am = this.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val tasks = am.runningAppProcesses
currentApp = tasks[0].processName
}
// Get only the app name name
return currentApp.split(".").last()
}
Sometimes the app name isn't the one displayed, for example the "gmail" app has the name "gm" when I tested it. The home screen name also changes from device to device
The logic to call it every few milliseconds to get the current foreground app is simple:
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
//Creating new thread for my service
//Always write your long running tasks in a separate thread, to avoid ANR
Thread(Runnable {
while (true) {
try {
Thread.sleep(10)
} catch (e: Exception) {
}
val currentApp = getForegroundApp()
if (currentApp != lastApp) {
println(currentApp)
// New app on front
lastApp = currentApp
println(currentApp)
// Do whatever you wan
}
}
}).start()
return START_STICKY
}
And that's it.
Background
I'm working on an app that can play some short videos.
I want to avoid accessing the Internet every time the user plays them, to make it faster and to lower the data usage.
The problem
Currently I've only found how to either play or download (it's just a file so I could download it like any other file).
Here's the code of playing a video file from URL (sample available here):
gradle
...
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.google.android.exoplayer:exoplayer-core:2.8.4'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.8.4'
...
manifest
<manifest package="com.example.user.myapplication" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<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" android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
activity_main.xml
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" tools:context=".MainActivity">
<com.google.android.exoplayer2.ui.PlayerView
android:id="#+id/playerView" android:layout_width="match_parent" android:layout_height="match_parent"
app:resize_mode="zoom"/>
</FrameLayout>
MainActivity.kt
class MainActivity : AppCompatActivity() {
private var player: SimpleExoPlayer? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun onStart() {
super.onStart()
playVideo()
}
private fun playVideo() {
player = ExoPlayerFactory.newSimpleInstance(this#MainActivity, DefaultTrackSelector())
playerView.player = player
player!!.addVideoListener(object : VideoListener {
override fun onVideoSizeChanged(width: Int, height: Int, unappliedRotationDegrees: Int, pixelWidthHeightRatio: Float) {
}
override fun onRenderedFirstFrame() {
Log.d("appLog", "onRenderedFirstFrame")
}
})
player!!.addListener(object : PlayerEventListener() {
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
super.onPlayerStateChanged(playWhenReady, playbackState)
when (playbackState) {
Player.STATE_READY -> Log.d("appLog", "STATE_READY")
Player.STATE_BUFFERING -> Log.d("appLog", "STATE_BUFFERING")
Player.STATE_IDLE -> Log.d("appLog", "STATE_IDLE")
Player.STATE_ENDED -> Log.d("appLog", "STATE_ENDED")
}
}
})
player!!.volume = 0f
player!!.playWhenReady = true
player!!.repeatMode = Player.REPEAT_MODE_ALL
player!!.playVideoFromUrl(this#MainActivity, "https://sample-videos.com/video123/mkv/720/big_buck_bunny_720p_1mb.mkv")
}
override fun onStop() {
super.onStop()
playerView.player = null
player!!.release()
player = null
}
abstract class PlayerEventListener : Player.EventListener {
override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters?) {}
override fun onSeekProcessed() {}
override fun onTracksChanged(trackGroups: TrackGroupArray?, trackSelections: TrackSelectionArray?) {}
override fun onPlayerError(error: ExoPlaybackException?) {}
override fun onLoadingChanged(isLoading: Boolean) {}
override fun onPositionDiscontinuity(reason: Int) {}
override fun onRepeatModeChanged(repeatMode: Int) {}
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {}
override fun onTimelineChanged(timeline: Timeline?, manifest: Any?, reason: Int) {}
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {}
}
companion object {
#JvmStatic
fun getUserAgent(context: Context): String {
val packageManager = context.packageManager
val info = packageManager.getPackageInfo(context.packageName, 0)
val appName = info.applicationInfo.loadLabel(packageManager).toString()
return Util.getUserAgent(context, appName)
}
}
fun SimpleExoPlayer.playVideoFromUri(context: Context, uri: Uri) {
val dataSourceFactory = DefaultDataSourceFactory(context, MainActivity.getUserAgent(context))
val mediaSource = ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri)
prepare(mediaSource)
}
fun SimpleExoPlayer.playVideoFromUrl(context: Context, url: String) = playVideoFromUri(context, Uri.parse(url))
fun SimpleExoPlayer.playVideoFile(context: Context, file: File) = playVideoFromUri(context, Uri.fromFile(file))
}
What I've tried
I've tried reading on the docs, and got those links (by asking about it here ) :
https://medium.com/google-exoplayer/downloading-streams-6d259eec7f95
https://medium.com/google-exoplayer/downloading-adaptive-streams-37191f9776e
So sadly, currently the only solution I can come up with, is to download the file on another thread, which will cause the device to have 2 connections to it, thus using twice the bandwidth.
The questions
How can I use ExoPlayer to play a video file, while also downloading it to some filepath ?
Is there a way to enable a caching mechanism (which uses the disk) on ExoPlayer to be activated for the exact same purpose?
Note: To make it clear. I do not want to download the file and only then play it.
EDIT: I've found a way to get&use the file from the API's cache (wrote about it here), but it appears that this is considered as unsafe (written here).
So, given the simple cache mechanism that the API of ExoPlayer supports, my current questions are:
If a file was cached, how can I use it in a safe manner?
If a file was partially cached (meaning we've downloaded a part of it), how can I continue preparing it (without actually playing it or waiting for the whole playback to finish) till I can use it (in a safe manner of course) ?
I've made a Github repository for this here. You can try it out.
I took a look at erdemguven's sample code here and seem to have something that works. This is by-and-large what erdemguven wrote, but I write to a file instead of a byte array and create the data source. I am thinking that since erdemguven, who is an ExoPlayer expert, presented this as the correct way to access cache, that my mods are also "correct" and do not break any rules.
Here is the code. getCachedData is the new stuff.
class MainActivity : AppCompatActivity(), CacheDataSource.EventListener, TransferListener {
private var player: SimpleExoPlayer? = null
companion object {
// About 10 seconds and 1 meg.
// const val VIDEO_URL = "https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4"
// About 1 minute and 5.3 megs
const val VIDEO_URL = "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4"
// The full movie about 355 megs.
// const val VIDEO_URL = "http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_1080p_60fps_normal.mp4"
// Use to download video other than the one you are viewing. See #3 test of the answer.
// const val VIDEO_URL_LIE = "http://file-examples.com/wp-content/uploads/2017/04/file_example_MP4_480_1_5MG.mp4"
// No changes in code deleted here.
//NOTE: I know I shouldn't use an AsyncTask. It's just a sample...
#SuppressLint("StaticFieldLeak")
fun tryShareCacheFile() {
// file is cached and ready to be used
object : AsyncTask<Void?, Void?, File>() {
override fun doInBackground(vararg params: Void?): File {
val tempFile = FilesPaths.FILE_TO_SHARE.getFile(this#MainActivity, true)
getCachedData(this#MainActivity, cache, VIDEO_URL, tempFile)
return tempFile
}
override fun onPostExecute(result: File) {
super.onPostExecute(result)
val intent = prepareIntentForSharingFile(this#MainActivity, result)
startActivity(intent)
}
}.execute()
}
private var mTotalBytesToRead = 0L
private var mBytesReadFromCache: Long = 0
private var mBytesReadFromNetwork: Long = 0
#WorkerThread
fun getCachedData(
context: Context, myCache: Cache?, url: String, tempfile: File
): Boolean {
var isSuccessful = false
val myUpstreamDataSource = DefaultHttpDataSourceFactory(ExoPlayerEx.getUserAgent(context)).createDataSource()
val dataSource = CacheDataSource(
myCache,
// If the cache doesn't have the whole content, the missing data will be read from upstream
myUpstreamDataSource,
FileDataSource(),
// Set this to null if you don't want the downloaded data from upstream to be written to cache
CacheDataSink(myCache, CacheDataSink.DEFAULT_BUFFER_SIZE.toLong()),
/* flags= */ 0,
/* eventListener= */ this
)
// Listen to the progress of the reads from cache and the network.
dataSource.addTransferListener(this)
var outFile: FileOutputStream? = null
var bytesRead = 0
// Total bytes read is the sum of these two variables.
mTotalBytesToRead = C.LENGTH_UNSET.toLong()
mBytesReadFromCache = 0
mBytesReadFromNetwork = 0
try {
outFile = FileOutputStream(tempfile)
mTotalBytesToRead = dataSource.open(DataSpec(Uri.parse(url)))
// Just read from the data source and write to the file.
val data = ByteArray(1024)
Log.d("getCachedData", "<<<<Starting fetch...")
while (bytesRead != C.RESULT_END_OF_INPUT) {
bytesRead = dataSource.read(data, 0, data.size)
if (bytesRead != C.RESULT_END_OF_INPUT) {
outFile.write(data, 0, bytesRead)
}
}
isSuccessful = true
} catch (e: IOException) {
// error processing
} finally {
dataSource.close()
outFile?.flush()
outFile?.close()
}
return isSuccessful
}
override fun onCachedBytesRead(cacheSizeBytes: Long, cachedBytesRead: Long) {
Log.d("onCachedBytesRead", "<<<<Cache read? Yes, (byte read) $cachedBytesRead (cache size) $cacheSizeBytes")
}
override fun onCacheIgnored(reason: Int) {
Log.d("onCacheIgnored", "<<<<Cache ignored. Reason = $reason")
}
override fun onTransferInitializing(source: DataSource?, dataSpec: DataSpec?, isNetwork: Boolean) {
Log.d("TransferListener", "<<<<Initializing isNetwork=$isNetwork")
}
override fun onTransferStart(source: DataSource?, dataSpec: DataSpec?, isNetwork: Boolean) {
Log.d("TransferListener", "<<<<Transfer is starting isNetwork=$isNetwork")
}
override fun onTransferEnd(source: DataSource?, dataSpec: DataSpec?, isNetwork: Boolean) {
reportProgress(0, isNetwork)
Log.d("TransferListener", "<<<<Transfer has ended isNetwork=$isNetwork")
}
override fun onBytesTransferred(
source: DataSource?,
dataSpec: DataSpec?,
isNetwork: Boolean,
bytesTransferred: Int
) {
// Report progress here.
if (isNetwork) {
mBytesReadFromNetwork += bytesTransferred
} else {
mBytesReadFromCache += bytesTransferred
}
reportProgress(bytesTransferred, isNetwork)
}
private fun reportProgress(bytesTransferred: Int, isNetwork: Boolean) {
val percentComplete =
100 * (mBytesReadFromNetwork + mBytesReadFromCache).toFloat() / mTotalBytesToRead
val completed = "%.1f".format(percentComplete)
Log.d(
"TransferListener", "<<<<Bytes transferred: $bytesTransferred isNetwork=$isNetwork" +
" $completed% completed"
)
}
// No changes below here.
}
Here is what I did to test this and this is by no means exhaustive:
Simply shared through email the video using the FAB. I received the video and was able to play it.
Turned off all network access on a physical device (airplane mode = on) and shared the video via email. When I turned the network back on (airplane mode = off), I received and was able to play the video. This shows that the video had to come from cache since the network was not available.
Changed the code so that instead of VIDEO_URL being copied from cache, I specified that VIDEO_URL_LIE should be copied. (The app still played only VIDEO_URL.) Since I had not downloaded the video for VIDEO_URL_LIE, the video was not in cache, so the app had to go out to the network for the video. I successfully received the correct video though email and was able to play it. This shows that the app can access the underlying asset if cache is not available.
I am by no means an ExoPlayer expert, so you will be able to stump me quickly with any questions that you may have.
The following code will track progress as the video is read and stored in a local file.
// Get total bytes if known. This is C.LENGTH_UNSET if the video length is unknown.
totalBytesToRead = dataSource.open(DataSpec(Uri.parse(url)))
// Just read from the data source and write to the file.
val data = ByteArray(1024)
var bytesRead = 0
var totalBytesRead = 0L
while (bytesRead != C.RESULT_END_OF_INPUT) {
bytesRead = dataSource.read(data, 0, data.size)
if (bytesRead != C.RESULT_END_OF_INPUT) {
outFile.write(data, 0, bytesRead)
if (totalBytesToRead == C.LENGTH_UNSET.toLong()) {
// Length of video in not known. Do something different here.
} else {
totalBytesRead += bytesRead
Log.d("Progress:", "<<<< Percent read: %.2f".format(totalBytesRead.toFloat() / totalBytesToRead))
}
}
}
you can use exoplayer's SimpleCache with LeastRecentlyUsedCacheEvictor to cache while streaming. Code would look something like.
temporaryCache = new SimpleCache(new File(context.getExternalCacheDir(), "player"), new LeastRecentlyUsedCacheEvictor(bytesToCache));
cacheSourceFactory = new CacheDataSourceFactory(temporaryCache, buildDataSourceFactory(), CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR);
Basically, whenever I launch a second activity and pause the application and relaunch app by tapping on its icon in launcher, app does not resumes on second activity but goes back to first activity. On top of that, if I pause the app(view in task manager), which app makes shows whole activity again like really fast(talking in milliseconds) and then continues to task manager.
Manifest file
<uses-permission android:name="android.permission.INTERNET" />
<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:replace="android:icon">
<activity
android:name=".Activity.MainActivity"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".Activity.ReferenceActivity"
android:configChanges="orientation|screenSize" /> <!-- Find a better solution for orientation change -->
<meta-data
android:name="preloaded_fonts"
android:resource="#array/preloaded_fonts" />
<activity android:name=".Activity.AboutActivity"></activity>
</application>
Since you would have noticed launchMode set to "singleTask", there is a reason behind it. My app contains recycler view. Its data is updated from second activity. Without launchMode set to singleTask, recyclerview data is not updated so to update the data I had to relaunch whole app just to see any data change. It is a temporary workaround but I am hoping if someone would help me on this matter as well.
About reycler view not updating, I have asked about that question countless times but never got a solution. Yes, I have gone through probably hundreds of similar problems on stack over flow and none of them work. Please bear in mind that I am beginner in Android development hence I do not have knowledge on advanced things like MvvM, RxJava or live data architecture.
P.S Just so any one gives a solution that I recall my data loader function again somewhere in onStart or on Resume, I have tried that countless times and it does not works. My app is not using fragments or any other advanced stuff just basic activities. Any help is appreciated!
Second activity
class ReferenceActivity : AppCompatActivity() {
private var dbHandler: PediaDatabase? = null
private var note = UserNotes()
private var noteExisted: Boolean = false
private var cardAdapterPos: Int? = null
private var title: String? = null
private var text: String? = null
private var sharingMenuOpened: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_reference)
val toolbarRef: Toolbar = findViewById(R.id.toolbarRefID)
setSupportActionBar(toolbarRef)
val toolbarTxtView = findViewById<TextView>(R.id.refToolbarTitleID)
supportActionBar!!.setDisplayShowTitleEnabled(false)
overridePendingTransition(R.anim.slide_in, R.anim.slide_out)
dbHandler = PediaDatabase(this)
val data = intent
if (!isNewNote) {
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN)
if (data != null) {
noteExisted = true
this.cardAdapterPos = data.extras.getInt("cardPosition")
cardID = data.extras.getInt("cardID")
existingNote = dbHandler!!.readNote(cardID)
text = existingNote.noteText
title = existingNote.noteTitle
refTitleID.setText(existingNote.noteTitle, TextView.BufferType.EDITABLE)
refTextID.setText(existingNote.noteText, TextView.BufferType.EDITABLE)
toolbarTxtView.text = "Created: " + existingNote.noteDate.toString()
}
} else {
toolbarTxtView.text = "New note"
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
}
}
override fun onResume() {
super.onResume()
sharingMenuOpened = false
}
override fun onPause() {
super.onPause()
if (!sharingMenuOpened)
saveNote()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.top_menu, menu)
val addItem: MenuItem = menu!!.findItem(R.id.add_note_menu)
val delItem: MenuItem = menu.findItem(R.id.delete_note_menu)
val shareButton: MenuItem = menu.findItem(R.id.shareID)
addItem.isVisible = false
delItem.isVisible = false
shareButton.isVisible = false
if (noteExisted) {
delItem.isVisible = true
shareButton.isVisible = true
}
return true
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
if (item!!.itemId == R.id.delete_note_menu) {
val deletionMsg = SweetAlertDialog(this, SweetAlertDialog.WARNING_TYPE)
deletionMsg.titleText = "Delete this note?"
deletionMsg.confirmText = "Yes"
deletionMsg.setCancelable(false)
deletionMsg.setConfirmClickListener(object : SweetAlertDialog.OnSweetClickListener {
override fun onClick(sweetAlertDialog: SweetAlertDialog?) {
dbHandler!!.deleteNote(cardID)
deletionMsg.dismissWithAnimation()
val successMsg = SweetAlertDialog(this#ReferenceActivity, SweetAlertDialog.SUCCESS_TYPE)
successMsg.setCancelable(false)
successMsg.titleText = "Note deleted!"
successMsg.setConfirmClickListener(object : SweetAlertDialog.OnSweetClickListener {
override fun onClick(sweetAlertDialog: SweetAlertDialog?) {
successMsg.dismissWithAnimation()
finish()
}
}).show()
}
})
deletionMsg.setCancelButton("No", object : SweetAlertDialog.OnSweetClickListener {
override fun onClick(sweetAlertDialog: SweetAlertDialog?) {
deletionMsg.dismissWithAnimation()
}
})
deletionMsg.show()
}
if (item.itemId == R.id.shareID) {
var sharingTitle: String = title!!.trim()
var sharingText: String = text!!.trim()
sharingMenuOpened = true
val sharingIntent = Intent(android.content.Intent.ACTION_SEND)
sharingIntent.type = "text/plain"
val shareBody: String? = sharingText
sharingIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, sharingTitle)
sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, shareBody)
startActivity(Intent.createChooser(sharingIntent, "Share to"))
}
return super.onOptionsItemSelected(item)
}
private fun saveNote() {
title = refTitleID.text.toString().trim()
text = refTextID.text.toString().trim()
if (existingNote.noteText == text && existingNote.noteTitle == title) {
finish()
} else {
if (noteExisted) {
if (TextUtils.isEmpty(title) && TextUtils.isEmpty(text)) {
dbHandler!!.deleteNote(cardID)
val parsedColor = Color.parseColor("#263238")
Toasty.Config.getInstance().setInfoColor(parsedColor).apply()
Toasty.info(this, "Note deleted", Toast.LENGTH_SHORT, true).show()
} else {
if (TextUtils.isEmpty(title))
title = "No title"
existingNote.noteTitle = title
existingNote.noteText = text
existingNote.noteDate = System.currentTimeMillis().toString()
dbHandler!!.updateNote(existingNote)
Toasty.success(this, "Note saved", Toast.LENGTH_SHORT, true).show()
startActivity(Intent(this, MainActivity::class.java))
finish()
}
} else {
if (TextUtils.isEmpty(title) && TextUtils.isEmpty(text)) {
finish()
} else {
if (TextUtils.isEmpty(title))
title = "No title"
note.noteTitle = title
note.noteText = text
note.noteDate = System.currentTimeMillis().toString()
dbHandler!!.createNote(note)
Toasty.success(this, "Note saved", Toast.LENGTH_SHORT, true).show()
startActivity(Intent(this, MainActivity::class.java))
finish()
}
}
}
}
}
You are invoking finish() in saveNote() method, which is being called in onPause(), hence every time your app goes into background, current activity is being finished (closed) due to invokation of finish() in it.
Remove the calls to finish() in saveNote() method, and you will be resumed to your current activity instead of landing on launcher/Main Activity.
You can try use this in MainActivty:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Intents.isActivityExpandedFromLauncherIcon(this)) {
finish()
} else {
setContentView(R.layout.activity_main)
}
}
upd:
public class Intents {
public static boolean isActivityExpandedFromLauncherIcon(#NonNull Activity activity) {
return !activity.isTaskRoot() && isActivityStartedFromLauncherIcon(activity.getIntent());
}
public static boolean isActivityStartedFromLauncherIcon(#Nullable Intent intent) {
return intent != null &&
intent.hasCategory(Intent.CATEGORY_LAUNCHER) &&
intent.getAction() != null &&
intent.getAction().equals(Intent.ACTION_MAIN);
}
}
Background
I've noticed there is a new function on the PackageManager called "getPackageInstaller" , with minAPI 21 (Lollipop).
I've reached the "PackageInstaller" class, and this is what it is written about it:
Offers the ability to install, upgrade, and remove applications on the
device. This includes support for apps packaged either as a single
"monolithic" APK, or apps packaged as multiple "split" APKs.
An app is delivered for installation through a
PackageInstaller.Session, which any app can create. Once the session
is created, the installer can stream one or more APKs into place until
it decides to either commit or destroy the session. Committing may
require user intervention to complete the installation.
Sessions can install brand new apps, upgrade existing apps, or add new
splits into an existing app.
Questions
What is this class used for? Is it even available for third party apps (I don't see any mentioning of this) ?
Can it really install apps?
Does it do it in the background?
What are the restrictions?
Does it require permissions? If so, which?
Is there any tutorial of how to use it?
OK, I've found some answers:
Can be used for installing/updating APK files, including split-APK files. Maybe even more.
Yes, but the user will need to confirm, one app after another.
Maybe if the app is built in.
Seems it requires to read the entire APK file/s before requesting the user to install.
Needs permission REQUEST_INSTALL_PACKAGES
Haven't found any, but someone showed me here how to install split-apk files, and here's how to do it for a single file using SAF, with and without PackageInstaller. Note that this is just a sample. I don't think it's a good practice to do it all on the UI thread.
manifest
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
package="com.android.apkinstalltest">
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<application tools:ignore="AllowBackup,GoogleAppIndexingWarning"
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">
<activity
android:name=".MainActivity"
android:label="#string/app_name"
android:theme="#style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<service android:name=".APKInstallService"/>
</application>
</manifest>
APKInstallService
class APKInstallService : Service() {
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
when (intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
Log.d("AppLog", "Requesting user confirmation for installation")
val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
startActivity(confirmationIntent)
} catch (e: Exception) {
}
}
PackageInstaller.STATUS_SUCCESS -> Log.d("AppLog", "Installation succeed")
else -> Log.d("AppLog", "Installation failed")
}
stopSelf()
return START_NOT_STICKY
}
override fun onBind(intent: Intent) = null
}
MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var packageInstaller: PackageInstaller
#TargetApi(Build.VERSION_CODES.O)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
packageInstaller = packageManager.packageInstaller
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "application/vnd.android.package-archive"
startActivityForResult(intent, 1)
}
// override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
// super.onActivityResult(requestCode, resultCode, resultData)
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && requestCode == 1 && resultCode == Activity.RESULT_OK && resultData != null) {
// val uri = resultData.data
// grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
// val intent = Intent(Intent.ACTION_INSTALL_PACKAGE)//
// .setDataAndType(uri, "application/vnd.android.package-archive")
// .putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
// .putExtra(Intent.EXTRA_RETURN_RESULT, false)
// .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
// startActivity(intent)
// }
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
super.onActivityResult(requestCode, resultCode, resultData)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && requestCode == 1 && resultCode == Activity.RESULT_OK && resultData != null) {
val uri = resultData.data ?: return
grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
val installParams = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
var cursor: Cursor? = null
var outputStream: OutputStream? = null
var inputStream: InputStream? = null
var session: PackageInstaller.Session? = null
try {
cursor = contentResolver.query(uri, null, null, null, null)
if (cursor != null) {
cursor.moveToNext()
val fileSize = cursor.getLong(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_SIZE))
val fileName = cursor.getString(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME))
installParams.setSize(fileSize)
cursor.close()
val sessionId = packageInstaller.createSession(installParams)
Log.d("AppLog", "Success: created install session [$sessionId] for file $fileName")
session = packageInstaller.openSession(sessionId)
outputStream = session.openWrite(System.currentTimeMillis().toString(), 0, fileSize)
inputStream = contentResolver.openInputStream(uri)
inputStream.copyTo(outputStream)
session.fsync(outputStream)
outputStream.close()
outputStream = null
inputStream.close()
inputStream = null
Log.d("AppLog", "Success: streamed $fileSize bytes")
val callbackIntent = Intent(applicationContext, APKInstallService::class.java)
val pendingIntent = PendingIntent.getService(applicationContext, 0, callbackIntent, 0)
session!!.commit(pendingIntent.intentSender)
session.close()
session = null
Log.d("AppLog", "install request sent. sessions:" + packageInstaller.mySessions)
}
} catch (e: Exception) {
Log.d("AppLog", "error:$e")
} finally {
outputStream?.close()
inputStream?.close()
session?.close()
cursor?.close()
}
}
}
}