/* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.appium.espressoserver.lib.http import androidx.test.espresso.Espresso import androidx.test.platform.app.InstrumentationRegistry import com.google.gson.GsonBuilder import fi.iki.elonen.NanoHTTPD import io.appium.espressoserver.lib.helpers.AndroidLogger import io.appium.espressoserver.lib.helpers.CustomFailureHandler import io.appium.espressoserver.lib.helpers.StringHelpers import io.appium.espressoserver.lib.helpers.setAccessibilityServiceState import io.appium.espressoserver.lib.http.response.AppiumResponse import io.appium.espressoserver.lib.http.response.BaseResponse import java.io.IOException import java.net.SocketException import java.util.LinkedHashMap const val DEFAULT_PORT = 6791 /** * Shared NanoHTTPD server logic; flavor modules supply [Server] subclasses that * extend this type and optionally mix in JUnit [org.junit.rules.TestRule] for Compose. */ abstract class ServerBase : NanoHTTPD(DEFAULT_PORT) { private var router: Router? = null @Volatile private var isStopRequestReceived: Boolean = false /** * No-op when Compose is not present; on Compose builds advances the main clock when the * driver strategy is Compose (see original Server implementation). */ protected abstract fun advanceComposeMainClockIfNeeded() private val syncComposeClock = Thread { while (!isStopRequestReceived) { advanceComposeMainClockIfNeeded() // Let Android run measure, draw and in general any other async operations. // AndroidComposeTestRule.android.kt:325 Thread.sleep(ANDROID_ASYNC_WAIT_TIME_MS) } } private fun buildFixedLengthResponse(response: BaseResponse): Response { val gsonBuilder = GsonBuilder().serializeNulls() return newFixedLengthResponse( response.httpStatus, "application/json", gsonBuilder.create().toJson(response), ) } fun run() { try { start() syncComposeClock.start() while (!isStopRequestReceived) { Thread.sleep(1000) } } finally { stop() syncComposeClock.join() } } override fun serve(session: IHTTPSession): Response { val files = LinkedHashMap() val response = try { session.parseBody(files) router!!.route(session.uri, session.method, files) } catch (e: Exception) { when (e) { is RuntimeException, is IOException, is ResponseException -> AppiumResponse(e) else -> throw e } } if (response is AppiumResponse) { if (response.httpStatus === Response.Status.OK) { AndroidLogger.info( "Responding to server with value: " + StringHelpers.abbreviate(response.value?.toString(), 300), ) } else { AndroidLogger.info("Responding to server with error: ${response.value}") } } return try { buildFixedLengthResponse(response) } catch (e: RuntimeException) { buildFixedLengthResponse(AppiumResponse(e)) } } @Throws(IOException::class) override fun start() { if (super.isAlive()) { try { super.stop() } catch (e: Exception) { // ignore the exception } } setAccessibilityServiceState() setCustomFailureHandler() try { super.start(SOCKET_READ_TIMEOUT, false) } catch (e: SocketException) { throw IllegalStateException( "The application under test must require android.permission.INTERNET " + "permission in its manifest", e, ) } AndroidLogger.info("\nRunning Appium Espresso Server at port $DEFAULT_PORT\n") router = Router(this) } private fun setCustomFailureHandler() = Espresso.setFailureHandler( CustomFailureHandler(InstrumentationRegistry.getInstrumentation().targetContext), ) override fun stop() { super.stop() AndroidLogger.info("\nStopping Appium Espresso at port $DEFAULT_PORT\n") } fun makeRequestForServerToStop() { isStopRequestReceived = true } private companion object { const val ANDROID_ASYNC_WAIT_TIME_MS = 10L } }