diff --git a/emulator.log b/emulator.log deleted file mode 100644 index e69de29bb..000000000 diff --git a/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/handlers/CreateSession.kt b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/handlers/CreateSession.kt index 5674bd92a..e9c323b04 100644 --- a/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/handlers/CreateSession.kt +++ b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/handlers/CreateSession.kt @@ -39,13 +39,13 @@ class CreateSession : RequestHandler { val shouldLaunchApp = (parsedCaps["autoLaunch"] ?: true) as Boolean if (shouldLaunchApp) { @Suppress("UNCHECKED_CAST") - startActivity( - StartActivityParams( - parsedCaps["appPackage"] as? String, - parsedCaps["appActivity"] as? String, - parsedCaps["intentOptions"] as? Map, - parsedCaps["activityOptions"] as? Map) - ) + startActivity(StartActivityParams( + parsedCaps["appPackage"] as? String, + parsedCaps["appActivity"] as? String, + parsedCaps["appLocale"] as? Map, + parsedCaps["intentOptions"] as? Map, + parsedCaps["activityOptions"] as? Map + )) } } catch (e: Exception) { throw SessionNotCreatedException(e) diff --git a/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/helpers/ActivityHelpers.kt b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/helpers/ActivityHelpers.kt index c3f9f2243..dd36f196e 100644 --- a/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/helpers/ActivityHelpers.kt +++ b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/helpers/ActivityHelpers.kt @@ -17,68 +17,92 @@ package io.appium.espressoserver.lib.helpers import android.app.Activity -import android.app.Instrumentation -import android.util.ArrayMap +import android.content.Context import android.os.Build - +import android.util.ArrayMap import androidx.test.platform.app.InstrumentationRegistry - -import io.appium.espressoserver.lib.handlers.exceptions.AppiumException +import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry +import androidx.test.runner.lifecycle.Stage import io.appium.espressoserver.lib.model.StartActivityParams +import io.appium.espressoserver.lib.model.mapToLocaleParams object ActivityHelpers { - // https://androidreclib.wordpress.com/2014/11/22/getting-the-current-activity/ val currentActivity: Activity - @Throws(AppiumException::class) get() { - try { - val activityThreadClass = Class.forName("android.app.ActivityThread") - val activityThread = activityThreadClass.getMethod("currentActivityThread").invoke(null) - val activitiesField = activityThreadClass.getDeclaredField("mActivities") - activitiesField.isAccessible = true - val activities = activitiesField.get(activityThread) as ArrayMap<*, *> - for (activityRecord in activities.values) { - val activityRecordClass = activityRecord.javaClass - val pausedField = activityRecordClass.getDeclaredField("paused") - pausedField.isAccessible = true - if (!pausedField.getBoolean(activityRecord)) { - val activityField = activityRecordClass.getDeclaredField("activity") - activityField.isAccessible = true - return activityField.get(activityRecord) as Activity + val method1 = fun(): Activity? { + // https://stackoverflow.com/questions/38737127/espresso-how-to-get-current-activity-to-test-fragments/58684943#58684943 + var result: Activity? = null + InstrumentationRegistry.getInstrumentation() + .runOnMainSync { + run { + result = ActivityLifecycleMonitorRegistry.getInstance() + .getActivitiesInStage(Stage.RESUMED) + .elementAtOrNull(0) + } + } + return result + } + val method2 = fun(): Activity? { + // https://androidreclib.wordpress.com/2014/11/22/getting-the-current-activity/ + try { + val activityThreadClass = Class.forName("android.app.ActivityThread") + val activityThread = activityThreadClass.getMethod("currentActivityThread") + .invoke(null) + val activitiesField = activityThreadClass.getDeclaredField("mActivities") + activitiesField.isAccessible = true + val activities = activitiesField.get(activityThread) as ArrayMap<*, *> + for (activityRecord in activities.values) { + val activityRecordClass = activityRecord.javaClass + val pausedField = activityRecordClass.getDeclaredField("paused") + pausedField.isAccessible = true + if (!pausedField.getBoolean(activityRecord)) { + val activityField = activityRecordClass.getDeclaredField("activity") + activityField.isAccessible = true + return activityField.get(activityRecord) as Activity + } } + } catch (e: Exception) { + // ignore + } + return null + } + for (method in listOf(method1, method2)) { + val result = method() + if (result != null) { + return result } - } catch (e: Exception) { - throw AppiumException(e) } - throw AppiumException("Failed to get current Activity") + throw IllegalStateException("Cannot retrieve the current activity") } /** * https://android.googlesource.com/platform/frameworks/base/+/master/tools/aapt/Resource.cpp#755 * - * @param instrumentation Instrumentation instance + * @param context Target context instance * @param pkg app package name * @param activity activity name shortcut to be qualified * @return The qualified activity name */ - private fun getFullyQualifiedActivityName(instrumentation: Instrumentation, - pkg: String?, activity: String): String { - val appPackage = pkg ?: instrumentation.targetContext.packageName + private fun getFullyQualifiedActivityName(context: Context, pkg: String?, activity: String): String { + val appPackage = pkg ?: context.packageName val dotPos = activity.indexOf(".") return (if (dotPos > 0) activity else "$appPackage${(if (dotPos == 0) "" else ".")}$activity") } - - fun startActivity(params: StartActivityParams) { if (params.appActivity == null && params.optionalIntentArguments == null) { throw IllegalArgumentException("Either activity name or intent options must be set") } val instrumentation = InstrumentationRegistry.getInstrumentation() + params.locale?.let { + changeLocale(instrumentation.targetContext.applicationContext, mapToLocaleParams(it).toLocale()) + } + val intent = if (params.optionalIntentArguments == null) { - val fullyQualifiedAppActivity = getFullyQualifiedActivityName(instrumentation, params.appPackage, params.appActivity!!) + val fullyQualifiedAppActivity = getFullyQualifiedActivityName(instrumentation.targetContext, + params.appPackage, params.appActivity!!) val defaultOptions = mapOf( "action" to "ACTION_MAIN", "flags" to "ACTIVITY_NEW_TASK", @@ -86,14 +110,14 @@ object ActivityHelpers { ) AndroidLogger.logger.info("Starting activity '$fullyQualifiedAppActivity' " + "with default options: $defaultOptions") - makeIntent(defaultOptions) + makeIntent(instrumentation.targetContext, defaultOptions) } else { AndroidLogger.logger.info("Staring activity with custom options: ${params.optionalIntentArguments}") - makeIntent(params.optionalIntentArguments) + makeIntent(instrumentation.targetContext, params.optionalIntentArguments) } if (params.optionalActivityArguments == null) { - instrumentation.startActivitySync(intent) + instrumentation.startActivitySync(intent) } else { makeActivityOptions(params.optionalActivityArguments).let { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { @@ -105,5 +129,4 @@ object ActivityHelpers { } } - } diff --git a/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/helpers/IntentHelpers.kt b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/helpers/IntentHelpers.kt index ac6c3114e..ae49aa49a 100644 --- a/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/helpers/IntentHelpers.kt +++ b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/helpers/IntentHelpers.kt @@ -17,9 +17,9 @@ package io.appium.espressoserver.lib.helpers import android.content.ComponentName +import android.content.Context import android.content.Intent import android.net.Uri -import androidx.test.platform.app.InstrumentationRegistry import kotlin.reflect.KClass import kotlin.reflect.full.cast @@ -152,7 +152,7 @@ private fun String.toComponentName(): ComponentName = ComponentName.unflattenFro * @throws IllegalArgumentException if required options are missing or * there is an issue with mapping value format */ -fun makeIntent(options: Map): Intent { +fun makeIntent(context: Context?, options: Map): Intent { val intent = Intent() var hasIntentInfo = false var data: Uri? = null @@ -177,8 +177,7 @@ fun makeIntent(options: Map): Intent { hasIntentInfo = true }, "className" to fun(key, value) { - intent.setClassName(InstrumentationRegistry.getInstrumentation().targetContext, - requireString(key, value)) + intent.setClassName(context!!, requireString(key, value)) }, "component" to fun(key, value) { intent.component = requireString(key, value).toComponentName() diff --git a/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/helpers/LocaleHelpers.kt b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/helpers/LocaleHelpers.kt new file mode 100644 index 000000000..b5069f26e --- /dev/null +++ b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/helpers/LocaleHelpers.kt @@ -0,0 +1,46 @@ +/* + * 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.helpers + +import android.content.Context +import android.os.Build +import android.os.Build.VERSION.SDK_INT +import android.os.LocaleList +import java.util.* + + +fun changeLocale(context: Context, locale: Locale) { + val res = context.applicationContext.resources + val config = res.configuration + + Locale.setDefault(locale) + if (SDK_INT >= Build.VERSION_CODES.O) { + Locale.setDefault(Locale.Category.DISPLAY, locale) + } + + if (SDK_INT >= Build.VERSION_CODES.N) { + val localeList = LocaleList(locale) + LocaleList.setDefault(localeList) + config.locales = localeList + context.createConfigurationContext(config) + } else { + config.setLocale(locale) + config.setLayoutDirection(locale) + @Suppress("DEPRECATION") + res.updateConfiguration(config, res.displayMetrics) + } +} diff --git a/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/model/LocaleParams.kt b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/model/LocaleParams.kt new file mode 100644 index 000000000..419c8f7e5 --- /dev/null +++ b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/model/LocaleParams.kt @@ -0,0 +1,34 @@ +/* + * 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.model + +import java.util.* + +// https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers +data class LocaleParams( + val language: String, + val country: String? = null, + val variant: String? = null +) : AppiumParams() { + fun toLocale(): Locale { + return Locale(language, country ?: "", variant ?: "") + } +} + +fun mapToLocaleParams(map: Map): LocaleParams { + return LocaleParams(map["language"] as String, map["country"] as? String, map["variant"] as? String) +} diff --git a/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/model/StartActivityParams.kt b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/model/StartActivityParams.kt index dfd0dd9be..467970f5b 100644 --- a/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/model/StartActivityParams.kt +++ b/espresso-server/app/src/androidTest/java/io/appium/espressoserver/lib/model/StartActivityParams.kt @@ -3,6 +3,7 @@ package io.appium.espressoserver.lib.model data class StartActivityParams( val appPackage: String? = null, val appActivity: String? = null, + val locale: Map? = null, val optionalIntentArguments: Map? = null, val optionalActivityArguments: Map? = null ) : AppiumParams() diff --git a/espresso-server/app/src/test/java/io/appium/espressoserver/test/helpers/IntentCreationTests.kt b/espresso-server/app/src/test/java/io/appium/espressoserver/test/helpers/IntentCreationTests.kt index 7159d9a74..71c19b975 100644 --- a/espresso-server/app/src/test/java/io/appium/espressoserver/test/helpers/IntentCreationTests.kt +++ b/espresso-server/app/src/test/java/io/appium/espressoserver/test/helpers/IntentCreationTests.kt @@ -30,38 +30,38 @@ class `Intent Creation Tests` { @Test(expected = Exception::class) fun `should throw if none of the required options have been provided`(){ - makeIntent(mapOf()) + makeIntent(null, mapOf()) } @Test(expected = Exception::class) fun `should throw if an invalid option name as been provided`(){ - makeIntent(mapOf( + makeIntent(null, mapOf( "foo" to "bar" )) } @Test fun `should create intent if options were set properly`(){ - assertEquals(makeIntent(mapOf( + assertEquals(makeIntent(null, mapOf( "action" to "io.appium.espresso" )).action, "io.appium.espresso") - assertEquals(makeIntent(mapOf( + assertEquals(makeIntent(null, mapOf( "data" to "content://contacts/people/1" )).data!!.toString(), "content://contacts/people/1") - assertEquals(makeIntent(mapOf( + assertEquals(makeIntent(null, mapOf( "type" to "image/png" )).type!!.toString(), "image/png") - assertEquals(makeIntent(mapOf( + assertEquals(makeIntent(null, mapOf( "categories" to "android.intent.category.APP_CONTACTS, android.intent.category.DEFAULT" )).categories.size, 2) } @Test fun `should create intent with valid intFlags`(){ - assertEquals(makeIntent(mapOf( + assertEquals(makeIntent(null, mapOf( "action" to "io.appium.espresso", "intFlags" to "0x0F" )).flags, 0x0F) @@ -69,7 +69,7 @@ class `Intent Creation Tests` { @Test fun `should create intent with valid flags`(){ - assertEquals(makeIntent(mapOf( + assertEquals(makeIntent(null, mapOf( "action" to "io.appium.espresso", "flags" to "GRANT_READ_URI_PERMISSION, FLAG_EXCLUDE_STOPPED_PACKAGES" )).flags, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_EXCLUDE_STOPPED_PACKAGES) @@ -77,7 +77,7 @@ class `Intent Creation Tests` { @Test fun `should create intent with valid single extra args`(){ - val intent = makeIntent(mapOf( + val intent = makeIntent(null, mapOf( "action" to "io.appium.espresso", "e" to mapOf( "bar" to "baz", @@ -101,7 +101,7 @@ class `Intent Creation Tests` { @Test fun `should create intent with valid array extra args`(){ - val intent = makeIntent(mapOf( + val intent = makeIntent(null, mapOf( "action" to "io.appium.espresso", "eia" to mapOf("intarr" to "1,2, 3"), "ela" to mapOf("longarr" to "4, 5, 6"), diff --git a/espresso-server/build.gradle b/espresso-server/build.gradle index e28797cb3..26673f794 100644 --- a/espresso-server/build.gradle +++ b/espresso-server/build.gradle @@ -14,7 +14,7 @@ buildscript { (propertyValue) ? Boolean.parseBoolean(propertyValue) : defaultValue } - ext.android_gradle_plugin_version = getStringProperty('appiumAndroidGradlePlugin', '4.0.0-beta05') + ext.android_gradle_plugin_version = getStringProperty('appiumAndroidGradlePlugin', '4.0.0-rc01') ext.kotlin_version = getStringProperty('appiumKotlin', '1.3.72') repositories { diff --git a/lib/desired-caps.js b/lib/desired-caps.js index f273d7e4e..854326667 100644 --- a/lib/desired-caps.js +++ b/lib/desired-caps.js @@ -40,6 +40,9 @@ let espressoCapConstraints = { }, activityOptions: { isObject: true + }, + appLocale: { + isObject: true, } };