Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add a possibility to set app locale #580

Merged
merged 5 commits into from
May 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file removed emulator.log
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ class CreateSession : RequestHandler<SessionParams, Session> {
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<String, Any?>,
parsedCaps["activityOptions"] as? Map<String, Any?>)
)
startActivity(StartActivityParams(
parsedCaps["appPackage"] as? String,
parsedCaps["appActivity"] as? String,
parsedCaps["appLocale"] as? Map<String, Any?>,
parsedCaps["intentOptions"] as? Map<String, Any?>,
parsedCaps["activityOptions"] as? Map<String, Any?>
))
}
} catch (e: Exception) {
throw SessionNotCreatedException(e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,83 +17,107 @@
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)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

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<String, Any>(
"action" to "ACTION_MAIN",
"flags" to "ACTIVITY_NEW_TASK",
"className" to fullyQualifiedAppActivity
)
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) {
Expand All @@ -105,5 +129,4 @@ object ActivityHelpers {
}
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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<String, Any?>): Intent {
fun makeIntent(context: Context?, options: Map<String, Any?>): Intent {
val intent = Intent()
var hasIntentInfo = false
var data: Uri? = null
Expand All @@ -177,8 +177,7 @@ fun makeIntent(options: Map<String, Any?>): 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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

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)
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Any?>): LocaleParams {
return LocaleParams(map["language"] as String, map["country"] as? String, map["variant"] as? String)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.appium.espressoserver.lib.model
data class StartActivityParams(
val appPackage: String? = null,
val appActivity: String? = null,
val locale: Map<String, Any?>? = null,
val optionalIntentArguments: Map<String, Any?>? = null,
val optionalActivityArguments: Map<String, Any?>? = null
) : AppiumParams()
Original file line number Diff line number Diff line change
Expand Up @@ -30,54 +30,54 @@ 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)
}

@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)
}

@Test
fun `should create intent with valid single extra args`(){
val intent = makeIntent(mapOf<String, Any>(
val intent = makeIntent(null, mapOf(
"action" to "io.appium.espresso",
"e" to mapOf(
"bar" to "baz",
Expand All @@ -101,7 +101,7 @@ class `Intent Creation Tests` {

@Test
fun `should create intent with valid array extra args`(){
val intent = makeIntent(mapOf<String, Any>(
val intent = makeIntent(null, mapOf<String, Any>(
"action" to "io.appium.espresso",
"eia" to mapOf("intarr" to "1,2, 3"),
"ela" to mapOf("longarr" to "4, 5, 6"),
Expand Down
2 changes: 1 addition & 1 deletion espresso-server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions lib/desired-caps.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ let espressoCapConstraints = {
},
activityOptions: {
isObject: true
},
appLocale: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

isObject: true,
}
};

Expand Down