I am trying to generate a thumbnail image from a PDF file in an android service with isolatedProcess = true. The reason for that is that the file is from an untrusted source and could be dangerous. The Android PDF Renderer documentation also refers to that issue see https://developer.android.com/reference/android/graphics/pdf/PdfRenderer.
If the file is from an untrusted source it is recommended to run the renderer in a separate, isolated process with minimal permissions to limit the impact of security exploits.
But my problem how can I pass the document from my application to the isolated service. If I use an intent and pass the document as a byteArray I get an TransactionTooLargeException. And if i try to pass the URI of the file and open an inputStream with the contentResolver I get an exception that an isolated process cannot access the contentResolver.
Update example with important code snippets
Android Manifest:
<application>
...
<service
android:name=".util.thumbnail.ThumbnailCreationService"
android:isolatedProcess="true"
android:exported="false"
android:process=":remote" />
</application>
Activity send Data to Service via Message Handler:
class MainActivity : Activity {
...
/**
* Class for interacting with the main interface of the service.
*/
private val mConnection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(
className: ComponentName,
service: IBinder
) {
mService = Messenger(service)
registerClient(mMessenger)
}
override fun onServiceDisconnected(className: ComponentName) {
mService = null
unregisterClient(mMessenger)
// As part of the sample, tell the user what happened.
Timber.d("Service disconnected")
}
}
fun sendMessageToService(fileUri:Uri{
mService?.let {
val msg = Message.obtain(
null,
MSG_CREATE_THUMBNAIL, this.hashCode(), 0
)
val bundle = Bundle()
// sending document URI to service
bundle.putString(ARG_DOCUMENT_DATA_URI, fileUri.toString())
// or the document data which leads to a TransactionToLargeException
// bundle.putByteArray(ARG_DOCUMENT_DATA, document)
bundle.putString(ARG_DOCUMENT_MIME_TYPE, mimeType)
msg.data = bundle
it.send(msg)
}
}
Service:
ThumbnailCreationService
class ThumbnailCreationService : Service() {
var mClients = ArrayList<Messenger>()
/**
* Handler of incoming messages from clients.
*/
internal class IncomingHandler(private val thumbnailCreationService: ThumbnailCreationService) : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
Timber.d("Retrieved client message: ${msg.what}")
when (msg.what) {
MSG_CREATE_THUMBNAIL -> {
msg.data?.let {
val uri = Uri.parse(it.getString(ARG_DOCUMENT_DATA_URI))
thumbnailCreationService.loadDocumentData(uri)
}
}
else -> super.handleMessage(msg)
}
}
}
protected fun loadDocumentData(uri: Uri) =
// this .contentResolver crashes because an isolatedProcess service has no access to the content resolver
this.contentResolver.openInputStream(uri)?.use {
IOHandler.readFileUriContent(it)
}
}
Stacktrace:
java.lang.SecurityException: Isolated process not allowed to call getContentProvider
at android.os.Parcel.createExceptionOrNull(Parcel.java:3011)
at android.os.Parcel.createException(Parcel.java:2995)
at android.os.Parcel.readException(Parcel.java:2978)
at android.os.Parcel.readException(Parcel.java:2920)
at android.app.IActivityManager$Stub$Proxy.getContentProvider(IActivityManager.java:5224)
at android.app.ActivityThread.acquireProvider(ActivityThread.java:7019)
at android.app.ContextImpl$ApplicationContentResolver.acquireUnstableProvider(ContextImpl.java:3420)
at android.content.ContentResolver.acquireUnstableProvider(ContentResolver.java:2526)
at android.content.ContentResolver.openTypedAssetFileDescriptor(ContentResolver.java:2011)
at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:1842)
at android.content.ContentResolver.openInputStream(ContentResolver.java:1518)
Related
I'm trying to send data using the DataClient from a phone to a watch.
Things I looked out for:
same package name
no build flavors on both modules
added service to the wear modules manifest
same path prefix
same signing config
I tried this sample project and copied parts over to my project. I just can't find any issues with it.
The sample project ran fine on my hardware, interestingly enough it wasn't working in the emulator. Therefore I tested my app also only with my hardware. (Pixel 6 Pro & Pixel Watch)
The sending data part seems to be working, as it behaves the same way as the sample project does.
How I send data from the phone:
class WearDataManager(val context: Context) {
private val dataClient by lazy { Wearable.getDataClient(context) }
companion object {
private const val CLIENTS_PATH = "/clients"
private const val CLIENT_LIST_KEY = "clientlist"
}
fun sendClientList(clientList: MutableList<String>) {
GlobalScope.launch {
try {
val request = PutDataMapRequest.create(CLIENTS_PATH).apply {
dataMap.putStringArray(CLIENT_LIST_KEY, arrayOf("clientList, test"))
}
.asPutDataRequest()
.setUrgent()
val result = dataClient.putDataItem(request).await()
Log.d("TAG", "DataItem saved: $result")
} catch (cancellationException: CancellationException) {
throw cancellationException
} catch (exception: Exception) {
Log.d("TAG", "Saving DataItem failed: $exception")
}
}
}
}
This is how I'm receiving data on the watch:
class WearableListenerService: WearableListenerService() {
companion object {
const val CLIENTS_PATH = "/clients"
}
override fun onCreate() {
super.onCreate()
Log.d("testing", "STARTED SERVICE")
}
override fun onDataChanged(dataEvents: DataEventBuffer) {
super.onDataChanged(dataEvents)
Log.d("testing", "RECEIVED $dataEvents")
}
}
Surprisingly "STARTED SERVICE" does not appear in the log when I start the app on the watch. For my understanding that means that the system isn't aware of the listeners existance and didn't register it. So something must be wrong with the manifest below.
This is the service inside the manifest on the watch:
<service android:name=".wear.communication.WearableListenerService"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.gms.wearable.DATA_CHANGED" />
<data
android:host="*"
android:pathPrefix="/clients"
android:scheme="wear" />
</intent-filter>
</service>
What am I missing here?
Turns out the sending part was the culprit after all. Be careful what scope you use or if you even want to use one at all. This function is being called inside of a worker in my code so it isn't an issue.
I completely modified the demo project above and with the help of this I found out why it wasn't working.
This is the working solution:
fun sendClientList(clientList: MutableList<String>) {
val request = PutDataMapRequest.create(CLIENTS_PATH).apply {
dataMap.putStringArray(CLIENT_LIST_KEY, arrayOf(clientList.joinToString()))
}
.asPutDataRequest()
.setUrgent()
val result = dataClient.putDataItem(request)
Log.d("TAG", "DataItem saved: $result")
}
I'd like to know a workaround to create a producer/consumer pattern in my Android application:
I have a dedicated device having a thermal printer, this app receives push notifications from FCM and print a receipt as soon as they arrive. Here it is the issue: multiple notifications at same time are not managed well, some are printed and some other not.
Printing is a call to startActivity(...) with an Intent containing an ACTION_VIEW with a Uri to open that allows printer service (external and not managed by me) to wake up.
So, I thought to create the well known producer/consumer pattern to enqueue all my Intent objects instead of calling startActivity inside FCM's onMessageReceived(...). How can I achieve that? What kind of service should it be implemented to consume this queue and send synchronously prints through these Intents?
I read docs on WorkManager APIs and I'm trying to write something like this below:
MyFirebaseMessagingService.kt
class MyFirebaseMessagingService : FirebaseMessagingService() {
override fun onMessageReceived(remoteMessage: RemoteMessage) {
remoteMessage.data.isNotEmpty().let { _ ->
try{
val content =
remoteMessage.data["content"]?.let { it1 -> Json.parseToJsonElement(it1).jsonObject }
content?.let { it ->
val title = it["title"]?.toString() ?: "Title example"
val body = it["message"]?.toString() ?: "Msg example"
val pushId = it["notificationId"]?.toString() ?: "42"
val data = it["data"]?.jsonObject
val intent = sendToPrinterIntent(data)
sendNotification(..., intent)
//startActivity(intent) //TODO add producer-consumer queue
PrintingWorker.enqueueWork(this, intent)
}
} catch (e: Exception){
Log.d("pushMessage", "Error in json data: $e")
}
}
}
private fun sendToPrinterIntent(data: JsonObject?): Intent {
return data?.let {
val body = getBody(it)
val uri = "customschema://q?text=$body"
return Intent(Intent.ACTION_VIEW, Uri.parse(uri))
} ?: Intent(Intent.ACTION_VIEW, Uri.parse("customschema://q?text="))
}
override fun onNewToken(token: String) {
Log.d("FCMtoken", "Refreshed token: $token")
}
private fun sendNotification(
messageBody: String,
messageTitle: String,
pushId: Int,
pendingIntent: Intent
) {
...
}
}
PrintingWorker.kt
class PrintingWorker(private val appContext: Context, workerParams: WorkerParameters) :
CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result {
//calls start activity and waits for it to finish
return withContext(Dispatchers.IO){
//appContext.startActivity()
workerParams.inputData.keyValueMap.forEach {
println("key: ${it.key} value: ${it.value}")
}
//setForeground()
Result.success()
}
}
override suspend fun getForegroundInfo(): ForegroundInfo {
return try {
ForegroundInfo(NOTIFICATION_ID,createNotification())
} catch (e: Exception) {
ForegroundInfo(NOTIFICATION_ID,Notification()) //example: can be ignored
}
}
private fun createNotification(): Notification {
return NotificationCompat...
}
companion object{
val TAG = "PrintingWorker"
val NOTIFICATION_ID = 4242
fun enqueueWork(context: Context, workData: Intent) {
val workRequest = OneTimeWorkRequest.Builder(PrintingWorker::class.java)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.setInputData(workDataOf(Pair("printingIntent",workData)))
.build()
WorkManager.getInstance(context).enqueue(workRequest)
}
}
}
As you can see in PrintingWorker, I'm not sure on how to let the WorkManager schedules and consumes the enqueued Intents. Idea of using this APIs is to allow consuming queue even device reboots, for example.
Any suggestions?
[EDIT] After reviewing possible solutions, I plan to achieve my goal by using Room + Foreground Service:
idea is to create entries in a table of the Room DB when a notification arrives in FCM's onReceiveMessage -> then a ForegroundService consume entries (deleting one at a time after printing data in it) by using Flow or something like that. Is it a more suitable solution? If yes, what should it be the right procedure to use Flow (or LiveData) to do so, avoiding unwanted results?
You'd need to convert the Bundle from Intent workData to Data data ...with Data.Builder.
We need to invoke a service when an outgoing call is placed so that we can use the target number to show certain additional information. As per Android documentation, CallRedirectionService should be used. However, after declaring a custom service as depicted in documentation, we find that the custom service is not getting triggered. Please let us know what we are doing wrong. Appreciate your help.
I referred to this link as well but not clear on the answer. There is a mention to role acquisition but I did not find that in Android documentation. Please direct me to the relevant page if available.
CallRedirectionService Implementation not working
Manifest.xml
<service android:name="<mypackage>.CustomCallService"
android:permission="android.permission.BIND_CALL_REDIRECTION_SERVICE">
<intent-filter>
<action android:name="android.telecom.CallRedirectionService"/>
</intent-filter>
</service>
Custom service code
#Override
public void onPlaceCall(#NonNull Uri handle, #NonNull PhoneAccountHandle initialPhoneAccount, boolean allowInteractiveResponse) {
System.out.println("Outgoing:" + initialPhoneAccount + ":" + handle); //Call does not reach here
placeCallUnmodified();
}
In Kotlin:
Your implementation of the CallRedirectionService seems correct. I understand that the only step missing is the role request and acquisition.
You can prompt the user to give you the CallRedirectionService role by using the RoleManager class.
In this example below, we are requesting this role as soon as the MainActivity is created:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (!isRedirection())
roleAcquire(RoleManager.ROLE_CALL_REDIRECTION)
}
}
The following functions shall be used:
private fun isRedirection(): Boolean {
return isRoleHeldByApp(RoleManager.ROLE_CALL_REDIRECTION)
}
private fun isRoleHeldByApp(roleName: String): Boolean {
val roleManager: RoleManager? = getSystemService(RoleManager::class.java)
return roleManager!!.isRoleHeld(roleName)
}
private fun roleAcquire(roleName: String) {
val roleManager: RoleManager?
if (roleAvailable(roleName)) {
roleManager = getSystemService(RoleManager::class.java)
val intent = roleManager.createRequestRoleIntent(roleName)
startActivityForResult(intent, 1)
} else {
Toast.makeText(
this,
"Redirection call with role in not available",
Toast.LENGTH_SHORT
).show()
}
}
private fun roleAvailable(roleName: String): Boolean {
val roleManager: RoleManager? = getSystemService(RoleManager::class.java)
return roleManager!!.isRoleAvailable(roleName)
}
In my Android app I have to show a list of available services on the network published by another machine (RPi 3B con Raspbian Stretch) using avahi 0.6.32 (Bonjour/zeroconf daemon for Linux). I obtain the list on Android phone using NsdManager.
But, during testing, I'm getting a strange behavior: when I switch WiFi off and back on in the phone, most of the times the services are discovered, then immediately lost and then rediscovered (instead of just discovered once) and all of this in less than a second.
This causes the list of services to briefly appear on screen, then disappear and finally reappear almost immediately, but it's still very noticeable. And it forces the services to be discovered and resolved twice. As I expect to have lots of phones connecting to several services in the same LAN, I want to avoid overloading the network.
I'm not sure if I'm doing something wrong or it's just the way NsdManager works on Android. To reduce possible sources of problem, I commented out the lines that resolve the services (leaving only the log messages) but the problem persisted (more then half of the times).
How can I solve it?
Sample extract from Logcat:
2019-09-26 04:33:50.262 27300-27420/com.example.myapp D/NsdHelper$initializeDiscoveryListener: Service discovery success: name: MyService-1490247, type: _mytype._tcp., host: null, port: 0, txtRecord:
2019-09-26 04:33:50.879 27300-27420/com.example.myapp D/NsdHelper$initializeDiscoveryListener: Service lost: name: MyService-1490247, type: _mytype._tcp., host: null, port: 0, txtRecord:
2019-09-26 04:33:50.970 27300-27420/com.example.myapp D/NsdHelper$initializeDiscoveryListener: Service discovery success: name: MyService-1490247, type: _mytype._tcp., host: null, port: 0, txtRecord:
I'm testing on a Samsung Note 8 with Android O. I tried with 2 different WiFi routers and the behavior is the same.
I'm using the following NsdHelper class (in Kotlin):
import android.content.Context
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import timber.log.Timber
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.collections.ArrayList
abstract class NsdHelper(val context: Context) {
// Declare DNS-SD related variables for service discovery
val nsdManager: NsdManager? = context.getSystemService(Context.NSD_SERVICE) as NsdManager?
private var discoveryListener: NsdManager.DiscoveryListener? = null
private var resolveListener: NsdManager.ResolveListener? = null
private var resolveListenerBusy = AtomicBoolean(false)
private var pendingNsdServices = ConcurrentLinkedQueue<NsdServiceInfo>()
var resolvedNsdServices: MutableList<NsdServiceInfo> =
Collections.synchronizedList(ArrayList<NsdServiceInfo>())
companion object {
// Type of services to look for
const val NSD_SERVICE_TYPE: String = "_mytype._tcp."
// Services' Names must start with this
const val NSD_SERVICE_NAME: String = "MyService-"
}
// Initialize Listeners
fun initializeNsd() {
// Initialize only resolve listener
initializeResolveListener()
}
// Instantiate DNS-SD discovery listener
// used to discover available Sonata audio servers on the same network
private fun initializeDiscoveryListener() {
Timber.d("Initialize DiscoveryListener")
// Instantiate a new DiscoveryListener
discoveryListener = object : NsdManager.DiscoveryListener {
override fun onDiscoveryStarted(regType: String) {
// Called as soon as service discovery begins.
Timber.d("Service discovery started: $regType")
}
override fun onServiceFound(service: NsdServiceInfo) {
// A service was found! Do something with it
Timber.d("Service discovery success: $service")
if ( service.serviceType == NSD_SERVICE_TYPE &&
service.serviceName.startsWith(NSD_SERVICE_NAME) ) {
// Both service type and service name are the ones we want
// If the resolver is free, resolve the service to get all the details
if (resolveListenerBusy.compareAndSet(false, true)) {
nsdManager?.resolveService(service, resolveListener)
} else {
// Resolver was busy. Add the service to the list of pending services
pendingNsdServices.add(service)
}
} else {
// Not our service. Log message but do nothing else
Timber.d("Not our Service - Name: ${service.serviceName}, Type: ${service.serviceType}")
}
}
override fun onServiceLost(service: NsdServiceInfo) {
Timber.d("Service lost: $service")
// If the lost service was in the queue of pending services, remove it
synchronized(pendingNsdServices) {
val iterator = pendingNsdServices.iterator()
while (iterator.hasNext()) {
if (iterator.next().serviceName == service.serviceName) iterator.remove()
}
}
// If the lost service was in the list of resolved services, remove it
synchronized(resolvedNsdServices) {
val iterator = resolvedNsdServices.iterator()
while (iterator.hasNext()) {
if (iterator.next().serviceName == service.serviceName) iterator.remove()
}
}
// Do the rest of the processing for the lost service
onNsdServiceLost(service)
}
override fun onDiscoveryStopped(serviceType: String) {
Timber.d("Discovery stopped: $serviceType")
}
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
Timber.e("Start Discovery failed: Error code: $errorCode")
stopDiscovery()
}
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
Timber.e("Stop Discovery failed: Error code: $errorCode")
nsdManager?.stopServiceDiscovery(this)
}
}
}
// Instantiate DNS-SD resolve listener to get extra information about the service
private fun initializeResolveListener() {
Timber.d("Initialize ResolveListener")
resolveListener = object : NsdManager.ResolveListener {
override fun onServiceResolved(service: NsdServiceInfo) {
Timber.d("Service Resolve Succeeded: $service")
// Register the newly resolved service into our list of resolved services
resolvedNsdServices.add(service)
// Process the newly resolved service
onNsdServiceResolved(service)
// Process the next service waiting to be resolved
resolveNextInQueue()
}
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
// Called when the resolve fails. Use the error code to debug.
Timber.e("Resolve failed: $serviceInfo - Error code: $errorCode")
// Process the next service waiting to be resolved
resolveNextInQueue()
}
}
}
// Start discovering services on the network
fun discoverServices() {
// Cancel any existing discovery request
stopDiscovery()
initializeDiscoveryListener()
// Start looking for available audio channels in the network
nsdManager?.discoverServices(NSD_SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener)
}
// Stop DNS-SD service discovery
fun stopDiscovery() {
if (discoveryListener != null) {
Timber.d("stopDiscovery() called")
try {
nsdManager?.stopServiceDiscovery(discoveryListener)
} finally {
}
discoveryListener = null
}
}
// Resolve next NSD service pending resolution
private fun resolveNextInQueue() {
// Get the next NSD service waiting to be resolved from the queue
val nextNsdService = pendingNsdServices.poll()
if (nextNsdService != null) {
// There was one. Send to be resolved.
nsdManager?.resolveService(nextNsdService, resolveListener)
} else {
// There was no pending service. Release the flag
resolveListenerBusy.set(false)
}
}
// Function to be overriden with custom logic for new service resolved
abstract fun onNsdServiceResolved(service: NsdServiceInfo)
// Function to be overriden with custom logic for service lost
abstract fun onNsdServiceLost(service: NsdServiceInfo)
}
On the init block of the View Model, I start the service discovery:
class MyViewModel(application: Application) : AndroidViewModel(application) {
// Get application context
private val myAppContext: Context = getApplication<Application>().applicationContext
// Declare NsdHelper object for service discovery
private val nsdHelper: NsdHelper? = object : NsdHelper(myAppContext) {
override fun onNsdServiceResolved(service: NsdServiceInfo) {
// A new network service is available
// Update list of available services
updateServicesList()
}
override fun onNsdServiceLost(service: NsdServiceInfo) {
// A network service is no longer available
// Update list of available services
updateServicesList()
}
}
// Block that is run when the view model is created
init {
Timber.d("init block called")
// Initialize DNS-SD service discovery
nsdHelper?.initializeNsd()
// Start looking for available audio channels in the network
nsdHelper?.discoverServices()
}
// Called when the view model is destroyed
override fun onCleared() {
Timber.d("onCleared called")
nsdHelper?.stopDiscovery()
super.onCleared()
}
private fun updateServicesList() {
// Put the logic here to show the services on screen
return
}
}
Note: Timber is a logging utility, almost a direct replacement for standard Log commands, but easier to use.
I'm developing an Android Library Module which is highly customizable in terms of UI. It would be really nice to have a script or some sort of automated process that takes screen shots of the running app, concatenate them and send by e-mail - so then I could quickly check if some change has messed with some UI component and/or have the most recent assets to update library READ-ME.
Any idea on how this could be performed?
My current idea
So far I've thought in adding code to programmatically take SS, store them on a temporary folder and, when all images has been collected, send them via some REST API to a server. I'd like to know if there is a better way to do that.
I ended up following my initial idea:
Based on this answer I've implemented a method that takes screenshots;
Base on this answer, I've implemented the API JavaMail capable of sending e-mails without the need of user interaction;
The combination of 1 and 2 can be found on my util library kotlin-components
Finally I've implemented UI tests that enters the desired state, takes the screen shots - saving them on external SD card - and, on the last step, it adds the SS as e-mail attachments sending to whatever I want to:
#RunWith(AndroidJUnit4::class)
#LargeTest
class UITestSearchSamples {
companion object {
private val SCREENSHOTS_DIRECTORY = "search-interface"
private val TIME_OUT = 3000L
private val WAITING_TIME = 1000L
#get:ClassRule
var disableAnimationsRule = DisableAnimationsRule()
}
private var finished = false
#get:Rule
var mActivityRule = ActivityTestRule(ActivityHomepage::class.java)
private var mMonitor: Instrumentation.ActivityMonitor? = null
#Before
fun setup() {
setWaitingPolice()
mMonitor = getInstrumentation().addMonitor(ActivitySearch::class.java.name, null, false)
}
private fun performWaitingTime() {
val idlingResource = ElapsedTimeIdlingResource(WAITING_TIME)
Espresso.registerIdlingResources(idlingResource)
}
private fun setWaitingPolice() {
IdlingPolicies.setMasterPolicyTimeout(TIME_OUT, TimeUnit.MILLISECONDS);
IdlingPolicies.setIdlingResourceTimeout(TIME_OUT, TimeUnit.MILLISECONDS);
}
#After
fun tearDown() {
closeSoftKeyboard()
performWaitingTime()
val activitySearch = getInstrumentation().waitForMonitorWithTimeout(mMonitor, TIME_OUT) as AppCompatActivity
activitySearch.takeScreenShot(location = DirectoryPath.EXTERNAL, path = SCREENSHOTS_DIRECTORY, openScreenShot = false, showToast = false)
activitySearch.finish()
if (finished) {
sendNotificationEmail(activitySearch)
}
}
private fun sendNotificationEmail(activitySearch: AppCompatActivity) {
try {
val sender = Sender("sender_email", "sender_password")
val email = Email(
"Hello world: SMTP Server from Android with Attachments",
"This is a sample e-mail sent via SMTP server from Android without the need of user interaction.",
mutableListOf("recipient_01", "recipient_02"),
File("${DirectoryPath.EXTERNAL.getValue(activitySearch)}/search-interface").listFiles()
)
activitySearch.sendEmail(sender, email)
} catch (e: Exception) {
Log.e("SENDER E-MAIL SLAVE", e.message, e)
}
}
#Test
fun launchSample01() {
onView(withId(R.id.btn_sample_01)).perform(click())
onView(withId(R.id.input)).perform(typeText("Diana"))
}
#Test
fun launchSample02() {
onView(withId(R.id.btn_sample_02)).perform(click())
onView(withId(R.id.input)).perform(typeText("Clark"))
}
#Test
fun launchSample03() {
onView(withId(R.id.btn_sample_03)).perform(click())
onView(withId(R.id.input)).perform(typeText("Diana"))
onView(withId(R.id.wrapper)).perform(click())
performWaitingTime()
onView(withId(R.id.input)).perform(typeText("a"))
finished = true
}
}