How can I test an Android Jetpack Compose composable that calls rememberLauncherForActivityResult?
For example:
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.compose.runtime.Composable
import androidx.compose.material3.Button
import androidx.compose.material3.Text
#Composable
fun PickFileButton() {
val launcher = rememberLauncherForActivityResult(CreateDocument("text/plain")) {
/* do something with the returned Uri */
}
Button(onClick={launcher.launch("default_file_name.txt")}) {
Text("Pick File")
}
}
Clicking the button launches another activity that the user uses to pick a file.
A test would look like:
import org.junit.Rule
import org.junit.Test
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
class TestPickFileButton {
#get:Rule val composeTestRule = createComposeRule()
#Test
fun testPickFileButton() {
composeTestRule.setContent { PickFileButton() }
composeTestRule.onNodeWithText("Pick File").performClick()
// now Android CreateDocument activity has launched
// and "composeTestRule" cannot interact with this activity
// rest of test eg to check the file was created
}
}
I cannot complete the test because the external activity has been launched and I can't interact with this activity with the ComposeContentTestRule object.
The solution involves using CompositionLocalProvider to override the default ActivityResultRegistry. This allows you to send a custom response with ActivityResultRegistry.dispatchResult instead of launching an activity.
I had to read the source code for the CreateDocument contract to understand what kind of Intent was expected to be returned.
A working solution for this particular question is:
import android.app.Activity
import android.content.Intent
import android.net.Uri
import androidx.activity.compose.LocalActivityResultRegistryOwner
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResultRegistry
import androidx.activity.result.ActivityResultRegistryOwner
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.core.app.ActivityOptionsCompat
import androidx.core.net.toFile
import org.junit.Rule
import org.junit.Test
#Composable
fun PickFileButton() {
val launcher =
rememberLauncherForActivityResult(CreateDocument("text/plain")) {
/* do something with the returned Uri */
}
Button(onClick = { launcher.launch("default_file_name.txt") }) { Text("Pick File") }
}
class TestPickFileButton {
#get:Rule val composeTestRule = createComposeRule()
#Test
fun testPickFileButton() {
val testUri = Uri.parse("uri_string_to_return")
composeTestRule.setContent {
// ActivityResultRegistry is responsible for handling the
// contracts and launching the activity
val registryOwner = ActivityResultRegistryOwner {
object : ActivityResultRegistry() {
override fun <I : Any?, O : Any?> onLaunch(
requestCode: Int,
contract: ActivityResultContract<I, O>,
input: I,
options: ActivityOptionsCompat?
) {
// don't launch an activity, just respond with the test Uri
val intent = Intent().setData(testUri)
this.dispatchResult(requestCode, Activity.RESULT_OK, intent)
}
}
}
CompositionLocalProvider(LocalActivityResultRegistryOwner provides registryOwner) {
// any composable inside this block will now use our mock ActivityResultRegistry
PickFileButton()
}
}
composeTestRule.onNodeWithText("Pick File").performClick()
// no activity is launched, PickFileButton will received testUri as a respose.
// rest of test eg to check the file was created
assert(testUri.toFile().exists())
}
}
Sources:
Discussion in Kotlin Lang Slack Chat,
ActivityResultRegistryTest's in Android repository,
CompositionLocalProvider
Related
I have a view that builds in JetpackCompose, I need to run the UI test in Android Studio by ParameterizedTest.
I run a simple ParameterizedTest in the below class but the Android Studio does not show successful state and I got this error.
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import io.mockk.spyk
import junit.framework.Assert.assertTrue
import org.junit.Rule
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
class MainTest {
#get:Rule
val composeTestRule = createComposeRule()
#ParameterizedTest
#ValueSource(ints = [2, 4, 6])
fun testIsEven(i: Int) {
assertTrue(i % 2 == 0)
}
}
Error:
1. No runnable methods
at org.junit.runners.ParentRunner.validate(ParentRunner.java:525)
at org.junit.runners.ParentRunner.<init>(ParentRunner.java:92)
at org.junit.runners.BlockJUnit4ClassRunner.<init>(BlockJUnit4ClassRunner.java:74)
at androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner.<init>(AndroidJUnit4ClassRunner.java:43)
at androidx.test.internal.runner.junit4.AndroidJUnit4Builder.runnerForClass(AndroidJUnit4Builder.java:65)
at org.junit.runners.model.RunnerBuilder.safeRunnerForClass(RunnerBuilder.java:70)
Can I run ParameterizedTest in UI test?
I'm creating a native module in react native.
his role is to connect to something and listen to data from this thing.
But.. I don't know how to observe my liveData property.
I can't use observeForever because I cannot use it in the background thread. and can't use it with reactContext looper.
I can't use it either with livedata.observe(owner) because my nativeModule don't have lifeCycleOwner. And if I create mine, I got the following error: Method handleLifeCycleEvent must be called on the main thread
I tried everything I can and I didn't find any tutorial about what I want. Everyone use sendEvent when a and I didn't find any tutorial is called but never in background. the official documentation doesn't give enought information too...
here my module:
package com.my.module
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.WritableMap
import com.facebook.react.bridge.Arguments
import com.facebook.react.modules.core.DeviceEventManagerModule
import com.someone.extsdk
class MyModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
override fun getName() = "MyModule"
#ReactMethod fun connect(id: String, promise: Promise) {
extsdk.connect(
onSuccess = { promise.resolve(null) },
onError = { e -> promise.reject("connect", e.toString())}
)
}
#ReactMethod fun disconnect() {
extsdk.disconnect()
}
#ReactMethod fun addListener(eventName: String) {
// How can I listen to extsdk.liveData?
}
#ReactMethod fun removeListeners(count: Int) {
// How can I remove listener?
}
private fun sendEvent(reactContext: ReactContext, eventName: String, params: WritableMap?) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit(eventName, params)
}
}
Can anyone please help. Am a noob to Android studio. My first major app, is a streaming media player. Problem, i wish to use foreground service, but i just cannot understand how to implement foreground service. I tried searching youtube, but not much on kotlin, and the same with the sites. Can anyone please give me a step by step guide for this.
This is my MainActivity below. The app is working, just missing the foreground service.
package com.example.al_bunyan
import android.app.Dialog
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.AnimationDrawable
import android.media.MediaPlayer
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.VideoView
import java.time.Instant
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
var web_btn = findViewById<Button>(R.id.link_btn)
web_btn.setOnClickListener {
var intent = Intent(this#MainActivity, AlbunyaWebview::class.java)
startActivity(intent)
}
}
fun video_check(view:View) {
val fm105 = findViewById<Button>(R.id.fm105_7)
val alert = Dialog(this#MainActivity)
alert.setContentView(R.layout.audio_play)
val video = alert.findViewById<VideoView>(R.id.video_alert)
val play = alert.findViewById<Button>(R.id.play)
val pause = alert.findViewById<Button>(R.id.pause)
val resume = alert.findViewById<Button>(R.id.resume)
if (fm105.isPressed) {
val video_1 = Uri.parse("http://albunyan.fm:8000/live")
video.setVideoURI(video_1)
alert.show()
play.setOnClickListener {
video.start()
}
pause.setOnClickListener {
video.pause()
}
resume.setOnClickListener {
video.start()
}
}
}
}
There are many resources available including code examples.
following are some of them
Android developer docs Services
Android developer docs Foreground services
Sample Android project on github LocationUpdatesForegroundService
I suggest first you read some theory from the first two resources and then try to learn from the code in the github project, which clearly shows how you can implement foreground service.
Our app needs to prevent users to record our screen for some important reasons. How to prevent screen-record on iOS and android?
It seems that we can get a call-back if users are recording the screen after iOS 11. Is there any method to use that call-back to realize screen-record preventing?
That is something which is not completely possible. There are 2 scenarios for Android OS
1 - When Android device is not rooted - You can use flag FLAG_SECURE which will prevent screenshot and video recording. You can read further here https://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#FLAG_SECURE
2 - When Android device is rooted - You can have a check programatically to know if device is rooted or not. If it is rooted then you can stop your application from running further. Here is the link to check for root device - Determining if an Android device is rooted programmatically?
Put this code in your MainActivity.kt and You're done :)
(The code isn't mine, I found it on Github)
import io.flutter.embedding.android.FlutterActivity
import android.os.Build
import android.view.ViewTreeObserver
import android.app.ActivityManager
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Log
import android.view.SurfaceView
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.WindowManager.LayoutParams
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity: FlutterActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val flutter_native_splash = true
var originalStatusBarColor = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
originalStatusBarColor = window.statusBarColor
window.statusBarColor = 0xffeaeaea.toInt()
}
val originalStatusBarColorFinal = originalStatusBarColor
if (!setSecureSurfaceView()) {
Log.e("MainActivity", "Could not secure the MainActivity!")
// React as appropriate.
}
getWindow().addFlags(LayoutParams.FLAG_SECURE)
getWindow().setFlags(LayoutParams.FLAG_SECURE,
LayoutParams.FLAG_SECURE)
SurfaceView(applicationContext).setSecure(true)
}
private fun setSecureSurfaceView(): Boolean {
val content = findViewById<ViewGroup>(android.R.id.content)
if (!isNonEmptyContainer(content)) {
return false
}
val splashView = content.getChildAt(0)
if (!isNonEmptyContainer(splashView)) {
return false
}
val flutterView = (splashView as ViewGroup).getChildAt(0)
if (!isNonEmptyContainer(flutterView)) {
return false
}
val surfaceView = (flutterView as ViewGroup).getChildAt(0)
if (surfaceView !is SurfaceView) {
return false
}
surfaceView.setSecure(true)
this.window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
return true
}
private fun isNonEmptyContainer(view: View): Boolean {
if (view !is ViewGroup) {
return false
}
if (view.childCount < 1) {
return false
}
return true
}
}
You can use the flutter_windowmanager plugin. Once you have added to your pubspec.yaml file then import it where you want to disable screenshot. Using
import 'package:flutter_windowmanager/flutter_windowmanager.dart';
Now add this line into your Stateful Widget.
Future<void> disableScreenShot() async {
await FlutterWindowManager.addFlags(FlutterWindowManager.FLAG_SECURE);
}
#override
void initState() {
disableScreenShot();
super.initState();
}
Now Normal Users can't take screenshots.
Why does isCanceled() return false after I call the cancel() function? In the source code I see what mIsCanceled variable must be set to true then I call cancel() function. Use this test to reproduce the error:
package some.package.com
import android.os.CancellationSignal
import junit.framework.TestCase.assertTrue
import org.junit.Test
class TestCancelationSignal {
#Test
fun SaveToFileEncryptData() {
val cancelationSignal = CancellationSignal()
cancelationSignal.cancel()
assertTrue(cancelationSignal.isCanceled)
}
}