Skip to content

Commit

Permalink
Merge pull request #19 from Q42/develop
Browse files Browse the repository at this point in the history
Merge dev to main
  • Loading branch information
yschermer authored Dec 19, 2022
2 parents 20e1dd6 + 9ac1043 commit 812a8ee
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 44 deletions.
4 changes: 2 additions & 2 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion .idea/saveactions_settings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Documentation

## Settings.Secure
Many accessibity settings can be queried using constants from [Settings.secure](https://developer.android.com/reference/android/provider/Settings.Secure).

Our AccessiblityCollector has a convenience method for it: `getSystemIntAsBool`

## Attempts to read accessibility settings

- Mono audio: Only found a private system setting: `MASTER_MONO`. These settings require root access.
47 changes: 29 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,28 @@ Add the Jitpack repo and include the library:

## Usage

1. Get the API key from [The Api project](https://github.com/Q42/accessibility-pipeline/tree/main/api). Use this key in the next step.
1. Get the API key
from [The Api project](https://github.com/Q42/accessibility-pipeline/tree/main/api). Use this key
in the next step.

1. Call `Q42Stats().runAsync(Context)` from anywhere in your app.
1. Call `Q42Stats().runAsync(Context)` from anywhere in your app.
```kotlin
class SampleApplication : Application() {
override fun onCreate() {
super.onCreate()
Q42Stats(
Q42StatsConfig(
apiKey = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
firestoreCollectionId = "theCollection",
firestoreCollectionId = "yourExistingFirestoreCollectionId",
// wait at least 7.5 days between data collections. the extra .5 is for time-of-day randomization
minimumSubmitIntervalSeconds = (60 * 60 * 24 * 7.5).toLong()
)
).runAsync(this.applicationContext)
}
}
```
This can be safely called from the main thread since all work (both collecting statistics and sending them to the server) are done on an IO thread.
This can be safely called from the main thread since all work (both collecting statistics and
sending them to the server) are done on an IO thread.

It is safe to call this function multiple times, as it will exit immediately if it is already
running or when a data collection interval has not passed yet.
Expand Down Expand Up @@ -76,20 +79,24 @@ versions of Android. If unsupported, the corresponding key is omitted.
### Accessibliity
| Key | Value | Notes | |-|-|-| | `isAccessibilityManagerEnabled` | bool | true when any
accessibility service (eg. Talkback) is Enabled | | `isClosedCaptioningEnabled` | bool | Live
transcription of any spoken audio (min sdk 19) | | `isTouchExplorationEnabled` | bool | Whether any
assistive feature is enabled where the user navigates the interface by touch. Most probably
TalkbBack, or similar | `isTalkBackEnabled` | bool | iOS: VoiceOver | `isSamsungTalkBackEnabled` |
bool | Specifically checks whether com.samsung.android.app.talkback.talkbackservice is enabled
| Key | Value | Notes |
|-|-|-|
| `isClosedCaptioningEnabled` | bool | Live transcription of any spoken audio (minSdk >= 19) |
| `isTouchExplorationEnabled` | bool | Whether any assistive feature is enabled where the user navigates the interface by touch. Most probably TalkBack, or similar
| `isTalkBackEnabled` | bool | iOS: VoiceOver
| `isSamsungTalkBackEnabled` | bool | Specifically checks whether com.samsung.android.app.talkback.talkbackservice is enabled
| `isSelectToSpeakEnabled` | bool | iOS: Speak Selection
| `isSwitchAccessEnabled` | bool | Control the device by a switch such as a foot pedal
| `isBrailleBackEnabled` | bool | Navigate the screen with an external Braille display
| `isVoiceAccessEnabled` | bool | iOS: Voice Control
| `fontScale` | float | Default value depends on device model. Some devices have a default font scaling of 1.1, for example |
| `displayScale` | float | Overall interface scaling ie. display density scaling. Default value may depend on device model (minSdk 24)|
| `isColorInversionEnabled` | bool | |
| `fontWeightAdjustment` | float | Default value is: 0. When bold text is enabled this value is greater than 0 (minSdk >= 31). Known issue: Always returns 0 on Samsung |
| `displayScale` | float | Overall interface scaling ie. display density scaling. Default value may depend on device model (minSdk >= 24)|
| `isMagnificationEnabled` | bool | Whether magnification is enabled (more specifically, whether magnification shortcuts are enabled) (minSdk >= 17). |
| `isColorInversionEnabled` | bool | Available starting from Android 5.0 (>=21) |
| `isColorBlindModeEnabled` | bool | |
| `isHighTextContrastEnabled` | bool | When enabled, all text has a thin outline. Available starting from Android 5.0 (>=21) |
| `isAnimationsDisabled` | bool | Can be disabled pre-Android 9 (<28) through Developer Options, starting from Android 9 possible to any user (minSdk >= 19). |
| `enabledAccessibilityServices` | Array\<String\> | List of enabled accessibility package names, eg ['com.accessibility.service1', 'nl.accessibility.service2'] |
### Preferences
Expand All @@ -107,12 +114,14 @@ bool | Specifically checks whether com.samsung.android.app.talkback.talkbackserv
### System
| Key | Value | Notes | |-|-|-| | `applicationId` | String | identifier for the app for which data
is collected, as set in the app's Manifest. iOS: bundleId | nl.hema.mobiel | | `defaultLanguage`|
en, nl, ... | | `sdkVersion` | int | 29 for Android
10. [See this list](https://source.android.com/setup/start/build-numbers)
|`manufacturer`|String|eg. `samsung`| |`modelName`|String| May be a marketing name, but more often
an internal code name. eg. `SM-G980F` for a particular variant of a Samsung Galaxy S10|
| Key | Value | Notes |
|-|-|-|
| `applicationId` | String | identifier for the app for which data is collected, as set in the app's Manifest. iOS: bundleId | nl.hema.mobiel |
| `defaultLanguage`| en-GB, nl-BE, nl, ... | If the country part (-BE) is not available, the value is just the language part ("nl")
| `sdkVersion` | int | 29 for Android 10. [See this list](https://source.android.com/setup/start/build-numbers)
|`manufacturer`|String|eg. `samsung`|
|`modelName`|String| May be a marketing name, but more often an internal code name. eg. `SM-G980F` for a particular variant of a Samsung Galaxy S10|
## Development
Expand All @@ -126,6 +135,8 @@ exceptions don't crash the implementing apps.
Catch Throwable; not Exception. Since Throwabl is the superclass of Exception, this will make the
lib more resilient to crashes.
For accessibility properties we want to track but could not find a property for, see [DOCUMENTATION.md](DOCUMENTATION.md)
### Setup
1. Get the API key
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ private fun sendPostRequestContent(conn: HttpURLConnection, data: String): Strin
try {
conn.outputStream.use { os ->
BufferedWriter(OutputStreamWriter(os, "UTF-8")).use { writer ->
writer.write(data.toString())
writer.write(data)
Q42StatsLogger.d(TAG, "Sending JSON: $data")
writer.flush()
}
Expand Down
36 changes: 18 additions & 18 deletions q42stats/src/main/java/com/q42/q42stats/library/Q42Stats.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ internal const val TAG = "Q42Stats"
* Version code for the data format that is sent to the server. Increment by 1 every time
* you add / remove / change a field in any of the Collector classes
*/
internal const val DATA_MODEL_VERSION = 3
internal const val DATA_MODEL_VERSION = 4

class Q42Stats(private val config: Q42StatsConfig) {

Expand Down Expand Up @@ -60,26 +60,25 @@ class Q42Stats(private val config: Q42StatsConfig) {
val currentMeasurement = collect(context)

val previousMeasurement: Map<String, Any?>? =
prefs.previousMeasurement?.let { deserializeMeasurement(it) }
val payload: Map<String, Any> = mapOf<String, Any?>(
"Stats Version" to "Android ${BuildConfig.LIB_BUILD_DATE}",
"currentMeasurement" to currentMeasurement,
"previousMeasurement" to previousMeasurement,
prefs.previousMeasurement?.let { deserializeMeasurement(it) }
val payload: Map<String, Any> = mapOf<String, Any?>(
"currentMeasurement" to currentMeasurement,
"previousMeasurement" to previousMeasurement,
).filterValueNotNull()
val serializedPayload = serializeMeasurement(payload.toQ42StatsApiFormat())
val responseBody = HttpService.sendStatsSync(
config,
serializedPayload,
prefs.lastBatchId
)
responseBody?.let { body ->
val responseBody = HttpService.sendStatsSync(
config,
serializedPayload,
prefs.lastBatchId
)
responseBody?.let { body ->
val batchId = JSONObject(body).getString("batchId") // throws if not found
prefs.lastBatchId = batchId
prefs.previousMeasurement = currentMeasurement
.toQ42StatsApiFormat()
.let { q42StatsCurrentMeasurement ->
serializeMeasurement(q42StatsCurrentMeasurement)
}
prefs.lastBatchId = batchId
prefs.previousMeasurement = currentMeasurement
.toQ42StatsApiFormat()
.let { q42StatsCurrentMeasurement ->
serializeMeasurement(q42StatsCurrentMeasurement)
}
}
} catch (e: Throwable) {
handleException(e)
Expand All @@ -103,6 +102,7 @@ class Q42Stats(private val config: Q42StatsConfig) {
private fun collect(context: Context): MutableMap<String, Serializable> {
val collected = mutableMapOf<String, Serializable>()

collected["Stats Version"] = "Android ${BuildConfig.LIB_BUILD_DATE}"
collected["Stats Model Version"] = DATA_MODEL_VERSION
collected["Stats timestamp"] = System.currentTimeMillis() / 1000L

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.q42.q42stats.library.collector

import android.accessibilityservice.AccessibilityServiceInfo
import android.annotation.TargetApi
import android.content.Context
import android.content.Context.ACCESSIBILITY_SERVICE
import android.content.Context.CAPTIONING_SERVICE
Expand All @@ -12,6 +11,7 @@ import android.provider.Settings
import android.util.DisplayMetrics
import android.view.accessibility.AccessibilityManager
import android.view.accessibility.CaptioningManager
import androidx.annotation.RequiresApi
import com.q42.q42stats.library.Q42StatsLogger
import com.q42.q42stats.library.TAG
import java.io.Serializable
Expand All @@ -31,7 +31,6 @@ internal object AccessibilityCollector {
it.resolveInfo?.serviceInfo?.name?.lowercase(Locale.ROOT)
}

put("isAccessibilityManagerEnabled", accessibilityManager.isEnabled)
put("isTouchExplorationEnabled", accessibilityManager.isTouchExplorationEnabled)
put(
"isTalkBackEnabled",
Expand All @@ -57,7 +56,18 @@ internal object AccessibilityCollector {
put(
"isVoiceAccessEnabled",
serviceNamesLower.any { it.contains("voiceaccess", ignoreCase = true) })
put("fontScale", configuration.fontScale)
put(
"fontScale",
configuration.fontScale
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
put("fontWeightAdjustment", configuration.fontWeightAdjustment)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
isMagnificationEnabled(context, serviceNamesLower)?.let {
put("isMagnificationEnabled", it)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
put(
"displayScale",
Expand All @@ -72,6 +82,12 @@ internal object AccessibilityCollector {
)
}
}
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1){
put(
"isAnimationsDisabled",
isAnimationsDisabled(context)
)
}

put("enabledAccessibilityServices", serviceNamesLower.toString())

Expand Down Expand Up @@ -104,9 +120,17 @@ internal object AccessibilityCollector {
it
)
}

getSystemIntAsBool(context, "high_text_contrast_enabled")?.let {
put(
"isHighTextContrastEnabled",
it
)
}

}

@TargetApi(Build.VERSION_CODES.KITKAT)
@RequiresApi(Build.VERSION_CODES.KITKAT)
private fun isClosedCaptioningEnabled(context: Context): Boolean? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
(context.getSystemService(CAPTIONING_SERVICE) as CaptioningManager).isEnabled
Expand All @@ -115,6 +139,35 @@ internal object AccessibilityCollector {
getSystemIntAsBool(context, "accessibility_captioning_enabled")
}

/**
* This is a best-effort means of checking whether magnification is enabled or not. It involves checking by which
* method the user can toggle magnification. Ideally, we want to read MagnificationController for this check, but this would
* require creating an AccessibilityService together with necessary permissions which this library should certainly not do.
*/
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
private fun isMagnificationEnabled(context: Context, serviceNames: List<String>): Boolean? = try {
val isMagnificationByTripleTapGesturesEnabled = getSystemIntAsBool(context,"accessibility_display_magnification_enabled") ?: false
val isMagnificationByVolumeButtonsEnabled = serviceNames.map { s -> s.lowercase() }.contains("com.example.android.apis.accessibility.magnificationservice")

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val isMagnificationByNavigationButtonEnabled =
Settings.Secure.getString(context.contentResolver, "accessibility_button_targets").lowercase().contains("com.android.server.accessibility.magnificationcontroller")

isMagnificationByTripleTapGesturesEnabled || isMagnificationByVolumeButtonsEnabled || isMagnificationByNavigationButtonEnabled
}else{
isMagnificationByTripleTapGesturesEnabled || isMagnificationByVolumeButtonsEnabled
}
} catch (e: Throwable) {
Q42StatsLogger.e(TAG, "Could not read magnification. Returning null", e)
null
}

@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
private fun isAnimationsDisabled(context: Context): Boolean =
(Settings.Global.getFloat(context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f) == 0f
&& Settings.Global.getFloat(context.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE, 1.0f) == 0f
&& Settings.Global.getFloat(context.contentResolver, Settings.Global.WINDOW_ANIMATION_SCALE, 1.0f) == 0f)

/**
* @return null when the value could not be read
*/
Expand Down

0 comments on commit 812a8ee

Please sign in to comment.