This commit is contained in:
MoXiang committed 2025-06-22 01:00:33 +08:00
1 parent 1889ce3062
commit 40f4d97dc0
34 files changed
+2130 -7

No files matched your search

+13 -1
View File
@@ -20,6 +20,7 @@ final String FRAMEWORK_PREBUILTS_DIR = "${ANDROID_TOP}/prebuilts/libs/"
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'com.google.protobuf' apply plugin: 'com.google.protobuf'
apply plugin: 'org.jetbrains.kotlin.android' apply plugin: 'org.jetbrains.kotlin.android'
apply plugin: 'kotlin-kapt'
android { android {
compileSdkVersion COMPILE_SDK compileSdkVersion COMPILE_SDK
@@ -155,6 +156,15 @@ android {
} }
// 启用增量注解处理(提升编译速度)
kapt {
useBuildCache = true
correctErrorTypes = true
javacOptions {
option("-Xmaxerrs", 1000)
}
}
allprojects { allprojects {
repositories { repositories {
// maven { url "../../../prebuilts/sdk/current/androidx/m2repository" } // maven { url "../../../prebuilts/sdk/current/androidx/m2repository" }
@@ -176,6 +186,8 @@ dependencies {
implementation project(':IconLoader') implementation project(':IconLoader')
implementation project(':systemUIPluginCore') implementation project(':systemUIPluginCore')
implementation project(':animationlib') implementation project(':animationlib')
implementation project(':Shared')
implementation project(':msdllib')
// implementation project(':UiTestsLibLauncher') // implementation project(':UiTestsLibLauncher')
// withQuickstepImplementation project(':SharedLibWrapper') // withQuickstepImplementation project(':SharedLibWrapper')
implementation 'com.google.protobuf:protobuf-javalite:4.26.1' implementation 'com.google.protobuf:protobuf-javalite:4.26.1'
@@ -199,7 +211,7 @@ dependencies {
api 'com.airbnb.android:lottie:6.6.6' api 'com.airbnb.android:lottie:6.6.6'
implementation 'com.google.dagger:dagger:2.56.2' implementation 'com.google.dagger:dagger:2.56.2'
annotationProcessor 'com.google.dagger:dagger-compiler:2.56.2' kapt 'com.google.dagger:dagger-compiler:2.56.2'
implementation 'com.google.android.material:material:1.12.0' implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.slice:slice-builders:1.0.0' implementation 'androidx.slice:slice-builders:1.0.0'
+1 -2
View File
@@ -19,5 +19,4 @@ PROTOBUF_DEPENDENCY=com.google.protobuf.nano:protobuf-javanano:3.1.0
BUILD_TOOLS_VERSION=28.0.3 BUILD_TOOLS_VERSION=28.0.3
COMPILE_SDK=android-36 COMPILE_SDK=android-36
#KOTLIN=org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10 KOTLIN=org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.21
KOTLIN=org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20
+1 -1
View File
@@ -28,7 +28,7 @@ android {
} }
dependencies { dependencies {
implementation("androidx.core:core:1.15.0") implementation("androidx.core:core:1.16.0")
implementation("androidx.annotation:annotation:1.9.1") implementation("androidx.annotation:annotation:1.9.1")
// api(project(":NexusLauncher:Flags")) // api(project(":NexusLauncher:Flags"))
} }
@@ -2,6 +2,161 @@ package com.android.launcher3
object Flags { object Flags {
@JvmStatic
fun enableShortcutDontSuggestApp(): Boolean {
return true
}
@JvmStatic
fun navigateToChildPreference(): Boolean {
return true
}
@JvmStatic
fun workSchedulerInWorkProfile(): Boolean {
return true
}
@JvmStatic
fun enableGeneratedPreviews(): Boolean {
return true
}
@JvmStatic
fun enableFocusOutline(): Boolean {
return true
}
@JvmStatic
fun privateSpaceAnimation(): Boolean {
return true
}
@JvmStatic
fun gridMigrationRefactor(): Boolean {
return true
}
@JvmStatic
fun enableDismissPredictionUndo(): Boolean {
return true
}
@JvmStatic
fun privateSpaceAddFloatingMaskView(): Boolean {
return true
}
@JvmStatic
fun msdlFeedback(): Boolean {
return true
}
@JvmStatic
fun privateSpaceRestrictAccessibilityDrag(): Boolean {
return true
}
@JvmStatic
fun enableTwolineToggle(): Boolean {
return true
}
@JvmStatic
fun oneGridRotationHandling(): Boolean {
return true
}
@JvmStatic
fun enableAdditionalHomeAnimations(): Boolean {
return true
}
@JvmStatic
fun enableLauncherOverviewInWindow(): Boolean {
return true
}
@JvmStatic
fun enableGridOnlyOverview(): Boolean {
return true
}
@JvmStatic
fun enableFallbackOverviewInWindow(): Boolean {
return true
}
@JvmStatic
fun enableMouseInteractionChanges(): Boolean {
return true
}
@JvmStatic
fun useSystemRadiusForAppWidgets(): Boolean {
return true
}
@JvmStatic
fun enableOverviewOnConnectedDisplays(): Boolean {
return true
}
@JvmStatic
fun enableFirstScreenBroadcastArchivingExtras(): Boolean {
return true
}
@JvmStatic
fun enableSmartspaceAsAWidget(): Boolean {
return true
}
@JvmStatic
fun enableLauncherBrMetricsFixed(): Boolean {
return true
}
@JvmStatic
fun enableCursorHoverStates(): Boolean {
return true
}
@JvmStatic
fun allAppsSheetForHandheld(): Boolean {
return true
}
@JvmStatic
fun enableOverviewIconMenu(): Boolean {
return true
}
@JvmStatic
fun enableContrastTiles(): Boolean {
return true
}
@JvmStatic
fun enableStateManagerProtoLog(): Boolean {
return true
}
@JvmStatic
fun enableLauncherVisualRefresh(): Boolean {
return true
}
@JvmStatic
fun enableTieredWidgetsByDefaultInPicker(): Boolean {
return true
}
@JvmStatic
fun enableTwoPaneLauncherSettings(): Boolean {
return true
}
@JvmStatic @JvmStatic
fun restoreArchivedAppIconsFromDb(): Boolean { fun restoreArchivedAppIconsFromDb(): Boolean {
return true return true
@@ -56,4 +211,94 @@ object Flags {
fun enableScalingRevealHomeAnimation(): Boolean { fun enableScalingRevealHomeAnimation(): Boolean {
return true return true
} }
@JvmStatic
fun enableTaskbarNoRecreate(): Boolean {
return true
}
@JvmStatic
fun enableTaskbarPinning(): Boolean {
return true
}
@JvmStatic
fun enableHomeTransitionListener(): Boolean {
return true
}
@JvmStatic
fun enableAllAppsButtonInHotseat(): Boolean {
return true
}
@JvmStatic
fun enableResponsiveWorkspace(): Boolean {
return true
}
@JvmStatic
fun letterFastScroller(): Boolean {
return true
}
@JvmStatic
fun enablePrivateSpace(): Boolean {
return true
}
@JvmStatic
fun privateSpaceAppInstallerButton(): Boolean {
return true
}
@JvmStatic
fun privateSpaceSysAppsSeparation(): Boolean {
return true
}
@JvmStatic
fun enableExpandingPauseWorkButton(): Boolean {
return true
}
@JvmStatic
fun allAppsBlur(): Boolean {
return true
}
@JvmStatic
fun multilineSearchBar(): Boolean {
return true
}
@JvmStatic
fun floatingSearchBar(): Boolean {
return true
}
@JvmStatic
fun privateSpaceRestrictItemDrag(): Boolean {
return true
}
@JvmStatic
fun extendibleThemeManager(): Boolean {
return true
}
@JvmStatic
fun removeAppsRefreshOnRightClick(): Boolean {
return true
}
@JvmStatic
fun enableAddAppWidgetViaConfigActivityV2(): Boolean {
return true
}
@JvmStatic
fun enableStrictMode(): Boolean {
return true
}
} }
+73
View File
@@ -0,0 +1,73 @@
// Copyright (C) 2024 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// 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 {
default_team: "trendy_team_motion",
default_applicable_licenses: ["Android-Apache-2.0"],
}
android_library {
name: "msdl",
manifest: "AndroidManifest.xml",
sdk_version: "system_current",
min_sdk_version: "31",
static_libs: [
"kotlinx_coroutines_android",
"androidx.annotation_annotation",
],
srcs: [
"src/**/*.java",
"src/**/*.kt",
],
kotlincflags: ["-Xjvm-default=all"],
}
android_library {
name: "msdl-tests-base",
libs: [
"android.test.base.stubs.system",
"androidx.test.core",
],
static_libs: [
"msdl",
"androidx.test.ext.junit",
"androidx.test.rules",
"testables",
"truth",
"kotlinx_coroutines_test",
"kotlin-test",
],
}
android_app {
name: "TestMSDLApp",
platform_apis: true,
static_libs: [
"msdl-tests-base",
],
}
android_test {
name: "msdl_tests",
manifest: "tests/AndroidManifest.xml",
static_libs: [
"msdl-tests-base",
],
srcs: [
"tests/src/**/*.kt",
],
kotlincflags: ["-Xjvm-default=all"],
test_suites: ["general-tests"],
}
+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.msdl">
<uses-permission android:name="android.permission.VIBRATE" />
</manifest>
+7
View File
@@ -0,0 +1,7 @@
{
"postsubmit": [
{
"name": "msdl_tests"
}
]
}
+33
View File
@@ -0,0 +1,33 @@
plugins {
id("com.android.library")
id("kotlin-android")
}
android {
namespace = "com.google.android.msdl"
compileSdk = 36
defaultConfig {
minSdk = 26
targetSdk = 36
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
}
sourceSets {
named("main") {
java.setSrcDirs(listOf("src"))
manifest.srcFile("AndroidManifest.xml")
res.setSrcDirs(listOf("res"))
}
}
kotlin {
jvmToolchain(21)//1.8(8)
}
}
dependencies {
implementation("androidx.core:core:1.16.0")
implementation("androidx.annotation:annotation:1.9.1")
}
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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 com.google.android.msdl.data.model
import android.os.VibrationEffect
/**
* A haptic composition as a list of [HapticCompositionPrimitive] and a [android.os.VibrationEffect]
* to use as a fallback.
*/
data class HapticComposition(
val primitives: List<HapticCompositionPrimitive>,
val fallbackEffect: VibrationEffect,
)
/**
* An abstraction of a haptic primitive in a composition that includes:
*
* @param[primitiveId] The id of the primitive.
* @param[scale] The scale of the primitive.
* @param[delayMillis] The delay of the primitive relative to the end of a previous primitive. Given
* in milliseconds.
*/
data class HapticCompositionPrimitive(
val primitiveId: Int,
var scale: Float = 1f,
var delayMillis: Int = 0,
)
@@ -0,0 +1,40 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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 com.google.android.msdl.data.model
/** Haptic tokens from the Multi-sensory Design Language (MSDL) */
enum class HapticToken {
NEGATIVE_CONFIRMATION_HIGH_EMPHASIS,
NEGATIVE_CONFIRMATION_MEDIUM_EMPHASIS,
POSITIVE_CONFIRMATION_HIGH_EMPHASIS,
POSITIVE_CONFIRMATION_MEDIUM_EMPHASIS,
POSITIVE_CONFIRMATION_LOW_EMPHASIS,
NEUTRAL_CONFIRMATION_HIGH_EMPHASIS,
NEUTRAL_CONFIRMATION_MEDIUM_EMPHASIS,
LONG_PRESS,
SWIPE_THRESHOLD_INDICATOR,
TAP_HIGH_EMPHASIS,
TAP_MEDIUM_EMPHASIS,
DRAG_THRESHOLD_INDICATOR,
DRAG_INDICATOR_CONTINUOUS,
DRAG_INDICATOR_DISCRETE,
TAP_LOW_EMPHASIS,
KEYPRESS_STANDARD,
KEYPRESS_SPACEBAR,
KEYPRESS_RETURN,
KEYPRESS_DELETE,
}
@@ -0,0 +1,155 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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 com.google.android.msdl.data.model
/** System-level tokens defined in the Multi-sensory Design Language (MSDL) */
enum class MSDLToken(
val hapticToken: HapticToken,
val soundToken: SoundToken,
val minimumFeedbackLevel: FeedbackLevel,
) {
/* Inform the user with emphasis that their current action FAILED to complete */
FAILURE_HIGH_EMPHASIS(
HapticToken.NEGATIVE_CONFIRMATION_HIGH_EMPHASIS,
SoundToken.FAILURE_HIGH_EMPHASIS,
FeedbackLevel.MINIMAL,
),
/* Inform the user that their current action FAILED to complete */
FAILURE(
HapticToken.NEGATIVE_CONFIRMATION_MEDIUM_EMPHASIS,
SoundToken.FAILURE,
FeedbackLevel.MINIMAL,
),
/* Inform the user their current action was completed SUCCESSFULLY */
SUCCESS(
HapticToken.POSITIVE_CONFIRMATION_HIGH_EMPHASIS,
SoundToken.SUCCESS,
FeedbackLevel.MINIMAL,
),
/* Inform the user that an ongoing activity has started */
START(HapticToken.NEUTRAL_CONFIRMATION_HIGH_EMPHASIS, SoundToken.START, FeedbackLevel.DEFAULT),
/* Inform the user that an ongoing activity has paused */
PAUSE(
HapticToken.NEUTRAL_CONFIRMATION_MEDIUM_EMPHASIS,
SoundToken.PAUSE,
FeedbackLevel.DEFAULT,
),
/* Inform the user that their previously started activity has stopped SUCCESSFULLY */
STOP(HapticToken.POSITIVE_CONFIRMATION_MEDIUM_EMPHASIS, SoundToken.STOP, FeedbackLevel.DEFAULT),
/* Inform the user that their previously started activity has cancelled SUCCESSFULLY */
CANCEL(
HapticToken.POSITIVE_CONFIRMATION_MEDIUM_EMPHASIS,
SoundToken.CANCEL,
FeedbackLevel.DEFAULT,
),
/* Inform the user that the state of an interactive component has been switched to on SUCCESSFULLY */
SWITCH_ON(
HapticToken.POSITIVE_CONFIRMATION_MEDIUM_EMPHASIS,
SoundToken.SWITCH_ON,
FeedbackLevel.DEFAULT,
),
/* Inform the user that the state of an interactive component has been switched to off SUCCESSFULLY */
SWITCH_OFF(
HapticToken.POSITIVE_CONFIRMATION_MEDIUM_EMPHASIS,
SoundToken.SWITCH_OFF,
FeedbackLevel.DEFAULT,
),
/* Inform the user the state of their device changed to unlocked SUCCESSFULLY */
UNLOCK(
HapticToken.POSITIVE_CONFIRMATION_LOW_EMPHASIS,
SoundToken.UNLOCK,
FeedbackLevel.DEFAULT,
),
/* Inform the user the state of their device changed to locked SUCCESSFULLY */
LOCK(HapticToken.POSITIVE_CONFIRMATION_LOW_EMPHASIS, SoundToken.LOCK, FeedbackLevel.DEFAULT),
/* Inform the user that their long-press gesture has resulted in the revealing of more contextual information */
LONG_PRESS(HapticToken.LONG_PRESS, SoundToken.LONG_PRESS, FeedbackLevel.MINIMAL),
/* Inform the user that their swipe gesture has reached a threshold that confirms navigation or the reveal of additional information. */
SWIPE_THRESHOLD_INDICATOR(
HapticToken.SWIPE_THRESHOLD_INDICATOR,
SoundToken.SWIPE_THRESHOLD_INDICATOR,
FeedbackLevel.MINIMAL,
),
/* Played when the user taps on a high-emphasis UI element */
TAP_HIGH_EMPHASIS(
HapticToken.TAP_HIGH_EMPHASIS,
SoundToken.TAP_HIGH_EMPHASIS,
FeedbackLevel.EXPRESSIVE,
),
/* Inform the user that their tap has resulted in a selection */
TAP_MEDIUM_EMPHASIS(
HapticToken.TAP_MEDIUM_EMPHASIS,
SoundToken.TAP_MEDIUM_EMPHASIS,
FeedbackLevel.DEFAULT,
),
/* Played when a users drag gesture reaches the maximum or minimum value */
DRAG_THRESHOLD_INDICATOR_LIMIT(
HapticToken.DRAG_THRESHOLD_INDICATOR,
SoundToken.DRAG_THRESHOLD_INDICATOR_LIMIT,
FeedbackLevel.DEFAULT,
),
/* Inform the user that their drag gesture has resulted in an incremental value change.
* For usage in haptic sliders that change continuously, this token can be played along with
* [InteractionProperties.DynamicVibrationScale] properties to control haptic scaling as a
* function of position and velocity.
*/
DRAG_INDICATOR_CONTINUOUS(
HapticToken.DRAG_INDICATOR_CONTINUOUS,
SoundToken.NO_SOUND,
FeedbackLevel.DEFAULT,
),
/* Inform the user that their drag gesture has resulted in a stepped value change.
* For usage in haptic sliders that change in discrete steps, this token can be played with
* [InteractionProperties.DynamicVibrationScale] properties to control haptic scaling as a
* function of position and velocity.
*/
DRAG_INDICATOR_DISCRETE(
HapticToken.DRAG_INDICATOR_DISCRETE,
SoundToken.DRAG_INDICATOR,
FeedbackLevel.DEFAULT,
),
/* Played when a user taps on any UI element that can be interacted with but is not otherwise defined */
TAP_LOW_EMPHASIS(
HapticToken.TAP_LOW_EMPHASIS,
SoundToken.TAP_LOW_EMPHASIS,
FeedbackLevel.EXPRESSIVE,
),
/* Played when the user touches a key on the keyboard that is otherwise undefined */
KEYPRESS_STANDARD(
HapticToken.KEYPRESS_STANDARD,
SoundToken.KEYPRESS_STANDARD,
FeedbackLevel.DEFAULT,
),
/* Played when the user touches the space key */
KEYPRESS_SPACEBAR(
HapticToken.KEYPRESS_SPACEBAR,
SoundToken.KEYPRESS_SPACEBAR,
FeedbackLevel.DEFAULT,
),
/* Played when the user touches the return key */
KEYPRESS_RETURN(HapticToken.KEYPRESS_RETURN, SoundToken.KEYPRESS_RETURN, FeedbackLevel.DEFAULT),
/* Played when the user touches the delete key */
KEYPRESS_DELETE(HapticToken.KEYPRESS_DELETE, SoundToken.KEYPRESS_DELETE, FeedbackLevel.DEFAULT),
}
/** Level of feedback that contains a token */
enum class FeedbackLevel {
NO_FEEDBACK,
MINIMAL,
DEFAULT,
EXPRESSIVE,
}
@@ -0,0 +1,44 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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 com.google.android.msdl.data.model
/** Sound tokens from the Multi-sensory Design Language (MSDL) */
enum class SoundToken {
FAILURE_HIGH_EMPHASIS,
FAILURE,
SUCCESS,
START,
PAUSE,
STOP,
CANCEL,
SWITCH_ON,
SWITCH_OFF,
UNLOCK,
LOCK,
LONG_PRESS,
SWIPE_THRESHOLD_INDICATOR,
TAP_HIGH_EMPHASIS,
TAP_MEDIUM_EMPHASIS,
DRAG_THRESHOLD_INDICATOR_LIMIT,
DRAG_INDICATOR,
TAP_LOW_EMPHASIS,
KEYPRESS_STANDARD,
KEYPRESS_SPACEBAR,
KEYPRESS_RETURN,
KEYPRESS_DELETE,
NO_SOUND,
}
@@ -0,0 +1,70 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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 com.google.android.msdl.data.repository
import androidx.annotation.VisibleForTesting
import com.google.android.msdl.data.model.HapticToken
import com.google.android.msdl.data.model.SoundToken
/**
* A repository of data for [HapticToken] and [SoundToken].
*
* The principle behind this repository is to hold the data for all tokens as a cache in memory.
* This is only suitable if the number of tokens and the data stored is manageable. The purpose of
* this design choice is to provide fast and easy access to the data when required to be played by
* UI interactions.
*/
sealed interface MSDLRepository {
/**
* Get the [MSDLHapticData] that corresponds to the given haptic reference token. This function
* needs to be fast since it will be called repeatedly to deliver feedback. If necessary, a
* caching strategy should be applied.
*
* @param[hapticToken] The [HapticToken] that points to the data.
* @return the data that corresponds to the token at the time this function is called.
*/
fun getHapticData(hapticToken: HapticToken): MSDLHapticData?
/**
* Get the [MSDLSoundData] that corresponds to the given sound reference token. This function
* needs to be fast since it will be called repeatedly to deliver feedback. If necessary, a
* caching strategy should be applied.
*
* @param[soundToken] The [SoundToken] that points to the data.
* @return the data that corresponds to the token at the time this function is called.
*/
fun getAudioData(soundToken: SoundToken): MSDLSoundData?
companion object {
@VisibleForTesting fun createRepository(): MSDLRepository = MSDLRepositoryImpl()
}
}
/** Representation of data contained in a [MSDLRepository] */
fun interface MSDLHapticData {
/** Retrieve the haptic data */
fun get(): Any?
}
/** Representation of data contained in a [MSDLRepository] */
fun interface MSDLSoundData {
/** Retrieve the sound data */
fun get(): Any?
}
@@ -0,0 +1,360 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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 com.google.android.msdl.data.repository
import android.os.VibrationEffect
import com.google.android.msdl.data.model.HapticComposition
import com.google.android.msdl.data.model.HapticCompositionPrimitive
import com.google.android.msdl.data.model.HapticToken
import com.google.android.msdl.data.model.SoundToken
/** A [MSDLRepository] that holds haptic compositions as haptic data. */
internal class MSDLRepositoryImpl : MSDLRepository {
override fun getAudioData(soundToken: SoundToken): MSDLSoundData? {
// TODO(b/345248875) Implement a caching strategy in accordance to the audio file strategy
return null
}
override fun getHapticData(hapticToken: HapticToken): MSDLHapticData? = HAPTIC_DATA[hapticToken]
companion object {
// Timings and amplitudes that recreate a composition of three SPIN primitives as a waveform
private val SPIN_TIMINGS = longArrayOf(20, 20, 3, 43, 20, 20, 3)
private val SPIN_AMPLITUDES = intArrayOf(40, 80, 40, 0, 40, 80, 40)
private const val SPIN_DELAY = 56L
private const val SPIN_BREAK = 10
private val SPIN_WAVEFORM_TIMINGS =
SPIN_TIMINGS + SPIN_DELAY + SPIN_TIMINGS + SPIN_DELAY + SPIN_TIMINGS
private val SPIN_WAVEFORM_AMPLITUDES =
SPIN_AMPLITUDES + SPIN_BREAK + SPIN_AMPLITUDES + SPIN_BREAK + SPIN_AMPLITUDES
private val HAPTIC_DATA: Map<HapticToken, MSDLHapticData> =
mapOf(
HapticToken.NEGATIVE_CONFIRMATION_HIGH_EMPHASIS to
MSDLHapticData {
HapticComposition(
listOf(
HapticCompositionPrimitive(
VibrationEffect.Composition.PRIMITIVE_SPIN,
scale = 1f,
delayMillis = 0,
),
HapticCompositionPrimitive(
VibrationEffect.Composition.PRIMITIVE_SPIN,
scale = 1f,
delayMillis = SPIN_DELAY.toInt(),
),
HapticCompositionPrimitive(
VibrationEffect.Composition.PRIMITIVE_SPIN,
scale = 1f,
delayMillis = SPIN_DELAY.toInt(),
),
),
VibrationEffect.createWaveform(
SPIN_WAVEFORM_TIMINGS,
SPIN_WAVEFORM_AMPLITUDES,
-1,
),
)
},
HapticToken.NEGATIVE_CONFIRMATION_MEDIUM_EMPHASIS to
MSDLHapticData {
HapticComposition(
listOf(
HapticCompositionPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK,
scale = 1f,
delayMillis = 0,
),
HapticCompositionPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK,
scale = 1f,
delayMillis = 114,
),
HapticCompositionPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK,
scale = 1f,
delayMillis = 114,
),
),
VibrationEffect.createWaveform(
longArrayOf(10, 10, 10, 114, 10, 10, 10, 114, 10, 10, 10),
intArrayOf(10, 255, 20, 0, 10, 255, 20, 0, 10, 255, 20),
-1,
),
)
},
HapticToken.POSITIVE_CONFIRMATION_HIGH_EMPHASIS to
MSDLHapticData {
HapticComposition(
listOf(
HapticCompositionPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK,
scale = 1f,
delayMillis = 0,
),
HapticCompositionPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK,
scale = 1f,
delayMillis = 114,
),
),
VibrationEffect.createWaveform(
longArrayOf(10, 10, 10, 114, 10, 10, 10),
intArrayOf(10, 255, 20, 0, 10, 255, 20),
-1,
),
)
},
HapticToken.POSITIVE_CONFIRMATION_MEDIUM_EMPHASIS to
MSDLHapticData {
HapticComposition(
listOf(
HapticCompositionPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK,
scale = 1f,
delayMillis = 0,
),
HapticCompositionPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK,
scale = 1f,
delayMillis = 52,
),
),
VibrationEffect.createWaveform(
longArrayOf(10, 10, 10, 52, 10, 10, 10),
intArrayOf(10, 255, 20, 0, 10, 255, 20),
-1,
),
)
},
HapticToken.POSITIVE_CONFIRMATION_LOW_EMPHASIS to
MSDLHapticData {
HapticComposition(
listOf(
HapticCompositionPrimitive(
VibrationEffect.Composition.PRIMITIVE_TICK,
scale = 1f,
delayMillis = 0,
),
HapticCompositionPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK,
scale = 1f,
delayMillis = 52,
),
),
VibrationEffect.createWaveform(
longArrayOf(5, 52, 10, 10, 10),
intArrayOf(100, 0, 10, 255, 20),
-1,
),
)
},
HapticToken.NEUTRAL_CONFIRMATION_HIGH_EMPHASIS to
MSDLHapticData {
HapticComposition(
listOf(
HapticCompositionPrimitive(
VibrationEffect.Composition.PRIMITIVE_THUD,
scale = 1f,
delayMillis = 0,
)
),
VibrationEffect.createWaveform(
longArrayOf(50, 100, 100, 50),
intArrayOf(5, 50, 20, 10),
-1,
),
)
},
HapticToken.NEUTRAL_CONFIRMATION_MEDIUM_EMPHASIS to
MSDLHapticData {
HapticComposition(
listOf(
HapticCompositionPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK,
scale = 1f,
delayMillis = 0,
)
),
VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK),
)
},
HapticToken.LONG_PRESS to
MSDLHapticData {
HapticComposition(
listOf(
HapticCompositionPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK,
scale = 1f,
delayMillis = 0,
)
),
VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK),
)
},
HapticToken.SWIPE_THRESHOLD_INDICATOR to
MSDLHapticData {
HapticComposition(
listOf(
HapticCompositionPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK,
scale = 0.7f,
delayMillis = 0,
)
),
VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK),
)
},
HapticToken.TAP_HIGH_EMPHASIS to
MSDLHapticData {
HapticComposition(
listOf(
HapticCompositionPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK,
scale = 0.7f,
delayMillis = 0,
)
),
VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK),
)
},
HapticToken.TAP_MEDIUM_EMPHASIS to
MSDLHapticData {
HapticComposition(
listOf(
HapticCompositionPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK,
scale = 0.5f,
delayMillis = 0,
)
),
VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK),
)
},
HapticToken.DRAG_THRESHOLD_INDICATOR to
MSDLHapticData {
HapticComposition(
listOf(
HapticCompositionPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK,
scale = 1f,
delayMillis = 0,
)
),
VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK),
)
},
HapticToken.DRAG_INDICATOR_CONTINUOUS to
MSDLHapticData {
HapticComposition(
List(size = 5) {
HapticCompositionPrimitive(
VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
scale = 0.3f,
delayMillis = 0,
)
},
VibrationEffect.createWaveform(
longArrayOf(10, 20, 20, 10),
intArrayOf(10, 30, 50, 10),
-1,
),
)
},
HapticToken.DRAG_INDICATOR_DISCRETE to
MSDLHapticData {
HapticComposition(
listOf(
HapticCompositionPrimitive(
VibrationEffect.Composition.PRIMITIVE_TICK,
scale = 0.5f,
delayMillis = 0,
)
),
VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK),
)
},
HapticToken.TAP_LOW_EMPHASIS to
MSDLHapticData {
HapticComposition(
listOf(
HapticCompositionPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK,
scale = 0.3f,
delayMillis = 0,
)
),
VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK),
)
},
HapticToken.KEYPRESS_STANDARD to
MSDLHapticData {
HapticComposition(
listOf(
HapticCompositionPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK,
scale = 0.5f,
delayMillis = 0,
)
),
VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK),
)
},
HapticToken.KEYPRESS_SPACEBAR to
MSDLHapticData {
HapticComposition(
listOf(
HapticCompositionPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK,
scale = 0.7f,
delayMillis = 0,
)
),
VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK),
)
},
HapticToken.KEYPRESS_RETURN to
MSDLHapticData {
HapticComposition(
listOf(
HapticCompositionPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK,
scale = 0.7f,
delayMillis = 0,
)
),
VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK),
)
},
HapticToken.KEYPRESS_DELETE to
MSDLHapticData {
HapticComposition(
listOf(
HapticCompositionPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK,
scale = 1f,
delayMillis = 0,
)
),
VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK),
)
},
)
}
}
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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 com.google.android.msdl.domain
import com.google.android.msdl.data.model.FeedbackLevel
import com.google.android.msdl.data.model.MSDLToken
import com.google.android.msdl.logging.MSDLEvent
/** An empty [MSDLPlayer] that was created without a [android.os.Vibrator] */
internal class EmptyMSDLPlayer : MSDLPlayer {
override fun getSystemFeedbackLevel(): FeedbackLevel = FeedbackLevel.NO_FEEDBACK
override fun playToken(token: MSDLToken, properties: InteractionProperties?) {}
override fun getHistory(): List<MSDLEvent> = listOf()
override fun toString(): String = "Empty MSDL player without a vibrator."
}
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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 com.google.android.msdl.domain
import android.os.VibrationAttributes
import androidx.annotation.FloatRange
/**
* Properties associated to an interaction that is currently occurring.
*
* The properties define parameters required to play the data associated with a
* [com.google.android.msdl.data.model.MSDLToken]. These can be dynamic, in the sense that they can
* be created as an interaction progresses throughout time.
*
* Each set of properties needs to define [VibrationAttributes] for a haptic effect to play with. If
* no properties are provided when playing a token, the effect will play with a default set of
* attributes with [VibrationAttributes.USAGE_TOUCH] usage.
*/
interface InteractionProperties {
/** [android.os.VibrationAttributes] for haptics in the interaction */
val vibrationAttributes: VibrationAttributes
/**
* Properties for a vibration that changes scale dynamically.
*
* The scale must be calculated at the time of calling the
* [com.google.android.msdl.domain.MSDLPlayer] API to play feedback. Use these properties for
* effects where vibration scales depend on temporal variables, such as position and velocity
* for slider haptics.
*
* @param[scale] The scale of the vibration at the time of calling. Must be between 0 and 1.
* @param[vibrationUsageId] Id used to create [android.os.VibrationAttributes]
*/
data class DynamicVibrationScale(
@FloatRange(from = 0.0, to = 1.0) val scale: Float,
override val vibrationAttributes: VibrationAttributes,
) : InteractionProperties
}
@@ -0,0 +1,127 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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 com.google.android.msdl.domain
import android.os.Vibrator
import android.util.Log
import com.google.android.msdl.data.model.FeedbackLevel
import com.google.android.msdl.data.model.HapticComposition
import com.google.android.msdl.data.model.MSDLToken
import com.google.android.msdl.data.repository.MSDLRepository
import com.google.android.msdl.data.repository.MSDLRepositoryImpl
import com.google.android.msdl.domain.MSDLPlayerImpl.Companion.REQUIRED_PRIMITIVES
import com.google.android.msdl.logging.MSDLEvent
import java.util.concurrent.Executor
import java.util.concurrent.Executors
/**
* Player of MSDL feedback.
*
* This player is central API to deliver audio and haptic feedback bundled and referenced by
* instances of [MSDLToken].
*/
interface MSDLPlayer {
// Current feedback level set in the system
fun getSystemFeedbackLevel(): FeedbackLevel
/**
* Play a [MSDLToken].
*
* @param[token] The [MSDLToken] to play. This will be used to fetch its corresponding haptic
* and sound data.
* @param[properties] [InteractionProperties] associated with the token requested to play. These
* properties can modify how a token plays (e.g.,
* [InteractionProperties.DynamicVibrationScale] for slider haptics in the
* [MSDLToken.DRAG_INDICATOR] token) and can be supplied if custom
* [android.os.VibrationAttributes] are required for haptic playback. If no properties are
* supplied, haptic feedback will play using USAGE_TOUCH [android.os.VibrationAttributes].
*/
fun playToken(token: MSDLToken, properties: InteractionProperties? = null)
/**
* Get the history of recent [MSDLEvent]s. The list can be useful to include in loggers and
* system dumps for debugging purposes.
*/
fun getHistory(): List<MSDLEvent>
companion object {
// TODO(b/355230334): remove once we have a system setting for the level
var SYSTEM_FEEDBACK_LEVEL = FeedbackLevel.DEFAULT
/**
* Create a new [MSDLPlayer].
*
* @param[vibrator] The [Vibrator] this player will use for haptic playback.
* @param[executor] An [Executor] to schedule haptic playback.
* @param[useHapticFeedbackForToken] A map that determines if a haptic fallback effect
* should be used to play haptics for a given [MSDLToken]. If null, the map will be
* created using the support information from the given vibrator.
*/
fun createPlayer(
vibrator: Vibrator?,
executor: Executor = Executors.newSingleThreadExecutor(),
useHapticFeedbackForToken: Map<MSDLToken, Boolean>? = null,
): MSDLPlayer {
// Return an empty player if no vibrator is available
if (vibrator == null) {
Log.w(
"MSDLPlayer",
"A null vibrator was used to create a MSDLPlayer. An empty player was created",
)
return EmptyMSDLPlayer()
}
// Create repository
val repository = MSDLRepositoryImpl()
// Determine the support for haptic primitives to know if fallbacks will be used.
// This can be provided by the client. If omitted, it will be determined from the
// supported primitives of the given vibrator.
val shouldUseFallbackForToken =
useHapticFeedbackForToken ?: createHapticFallbackDecisionMap(vibrator, repository)
return MSDLPlayerImpl(repository, vibrator, executor, shouldUseFallbackForToken)
}
private fun createHapticFallbackDecisionMap(
vibrator: Vibrator,
repository: MSDLRepository,
): Map<MSDLToken, Boolean> {
val supportedPrimitives =
REQUIRED_PRIMITIVES.associateWith { vibrator.arePrimitivesSupported(it).first() }
return MSDLToken.entries.associateWith { token ->
// For each token, determine if the haptic data from the repository
// should use the fallback effect.
val hapticComposition =
repository.getHapticData(token.hapticToken)?.get() as? HapticComposition
hapticComposition?.shouldPlayFallback(supportedPrimitives) ?: false
}
}
}
}
fun HapticComposition.shouldPlayFallback(supportedPrimitives: Map<Int, Boolean>): Boolean {
primitives.forEach { primitive ->
val isSupported = supportedPrimitives[primitive.primitiveId]
if (isSupported == null || isSupported == false) {
return true
}
}
return false
}
@@ -0,0 +1,149 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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 com.google.android.msdl.domain
import android.os.Build
import android.os.VibrationAttributes
import android.os.VibrationEffect
import android.os.Vibrator
import com.google.android.msdl.data.model.FeedbackLevel
import com.google.android.msdl.data.model.HapticComposition
import com.google.android.msdl.data.model.MSDLToken
import com.google.android.msdl.data.repository.MSDLRepository
import com.google.android.msdl.logging.MSDLEvent
import com.google.android.msdl.logging.MSDLHistoryLogger
import com.google.android.msdl.logging.MSDLHistoryLoggerImpl
import java.util.concurrent.Executor
/**
* Implementation of the MSDLPlayer.
*
* At the core, the player is in charge of delivering haptic and audio feedback closely in time.
*
* @param[repository] Repository to retrieve audio and haptic data.
* @param[executor] An [Executor] used to schedule haptic playback.
* @param[vibrator] Instance of the default [Vibrator] on the device.
* @param[useHapticFallbackForToken] A map that determines if the haptic fallback effect should be
* used for a given token.
*/
internal class MSDLPlayerImpl(
private val repository: MSDLRepository,
private val vibrator: Vibrator,
private val executor: Executor,
private val useHapticFallbackForToken: Map<MSDLToken, Boolean?>,
) : MSDLPlayer {
/** A logger to keep a history of playback events */
private val historyLogger = MSDLHistoryLoggerImpl(MSDLHistoryLogger.HISTORY_SIZE)
// TODO(b/355230334): This should be retrieved from the system Settings
override fun getSystemFeedbackLevel(): FeedbackLevel = MSDLPlayer.SYSTEM_FEEDBACK_LEVEL
override fun playToken(token: MSDLToken, properties: InteractionProperties?) {
// Don't play the data for the token if the current feedback level is below the minimal
// level of the token
if (getSystemFeedbackLevel() < token.minimumFeedbackLevel) return
// Play the data for the token with the given properties
playData(token, properties)
}
private fun playData(token: MSDLToken, properties: InteractionProperties?) {
// Gather the data from the repositories
val hapticData = repository.getHapticData(token.hapticToken)
val soundData = repository.getAudioData(token.soundToken)
// Nothing to play
if (hapticData == null && soundData == null) return
if (soundData == null) {
// Play haptics only
// 1. Create the effect
val composition: HapticComposition? = hapticData?.get() as? HapticComposition
val effect =
if (useHapticFallbackForToken[token] == true) {
composition?.fallbackEffect
} else {
when (properties) {
is InteractionProperties.DynamicVibrationScale -> {
composition?.composeIntoVibrationEffect(
scaleOverride = properties.scale
)
}
else -> composition?.composeIntoVibrationEffect() // compose as-is
}
}
// 2. Deliver the haptics with or without attributes
if (effect == null || !vibrator.hasVibrator()) return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val attributes =
if (properties?.vibrationAttributes != null) {
properties.vibrationAttributes
} else {
VibrationAttributes.Builder()
.setUsage(VibrationAttributes.USAGE_TOUCH)
.build()
}
executor.execute { vibrator.vibrate(effect, attributes) }
} else {
executor.execute { vibrator.vibrate(effect) }
}
// 3. Log the event
historyLogger.addEvent(MSDLEvent(token, properties))
} else {
// TODO(b/345248875): Play audio and haptics
}
}
override fun getHistory(): List<MSDLEvent> = historyLogger.getHistory()
override fun toString(): String =
"""
Default MSDL player implementation.
Vibrator: $vibrator
Repository: $repository
"""
.trimIndent()
companion object {
val REQUIRED_PRIMITIVES =
listOf(
VibrationEffect.Composition.PRIMITIVE_SPIN,
VibrationEffect.Composition.PRIMITIVE_THUD,
VibrationEffect.Composition.PRIMITIVE_TICK,
VibrationEffect.Composition.PRIMITIVE_CLICK,
VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
)
}
}
fun HapticComposition.composeIntoVibrationEffect(
scaleOverride: Float? = null,
delayOverride: Int? = null,
): VibrationEffect? {
val effectComposition = VibrationEffect.startComposition()
primitives.forEach { primitive ->
effectComposition.addPrimitive(
primitive.primitiveId,
scaleOverride ?: primitive.scale,
delayOverride ?: primitive.delayMillis,
)
}
return effectComposition.compose()
}
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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 com.google.android.msdl.logging
import com.google.android.msdl.data.model.MSDLToken
import com.google.android.msdl.domain.InteractionProperties
/**
* A summary that represents a MSDL event. The event summarizes the delivery of playback from a
* [MSDLToken] along with optional [InteractionProperties].
*
* @param[tokenName] The name of the [MSDLToken] played.
* @param[properties] The text representation of [InteractionProperties] used to play the token.
* @param[timeStamp] A formatted time stamp for when the event occurred. The format for this time
* stamp is [MSDLHistoryLogger.DATE_FORMAT]
*/
data class MSDLEvent(val tokenName: String, val properties: String?, val timeStamp: String) {
constructor(
token: MSDLToken,
properties: InteractionProperties?,
) : this(
token.name,
properties?.toString(),
MSDLHistoryLogger.DATE_FORMAT.format(System.currentTimeMillis()),
)
override fun toString(): String = "$timeStamp | token: $tokenName | properties: $properties"
}
@@ -0,0 +1,40 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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 com.google.android.msdl.logging
import java.text.SimpleDateFormat
import java.util.Locale
/** A logging component to keep track of recent [MSDLEvent]s */
interface MSDLHistoryLogger {
/**
* Add an event to the history.
*
* @param[event] The event to log.
*/
fun addEvent(event: MSDLEvent)
/** Get the history of latest events. */
fun getHistory(): List<MSDLEvent>
companion object {
const val HISTORY_SIZE = 20
val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
}
}
@@ -0,0 +1,39 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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 com.google.android.msdl.logging
import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.Companion.PACKAGE_PRIVATE
import java.util.ArrayDeque
import java.util.Deque
@VisibleForTesting(otherwise = PACKAGE_PRIVATE)
class MSDLHistoryLoggerImpl(private val maxHistorySize: Int) : MSDLHistoryLogger {
// Use an [ArrayDequeue] with a fixed size as the history structure
private val history: Deque<MSDLEvent> = ArrayDeque(maxHistorySize)
override fun addEvent(event: MSDLEvent) {
// Keep the history as a FIFO structure
if (history.size == maxHistorySize) {
history.removeFirst()
}
history.addLast(event)
}
override fun getHistory(): List<MSDLEvent> = history.toList()
}
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 2024 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.msdl.tests">
<instrumentation
android:name="android.testing.TestableInstrumentation"
android:label="Tests for the public MSDL Lib"
android:targetPackage="com.google.android.msdl.tests"/>
</manifest>
@@ -0,0 +1,41 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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 com.google.android.msdl.data.repository
import com.google.android.msdl.data.model.HapticComposition
import com.google.android.msdl.data.model.HapticToken
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class MSDLRepositoryImplTest {
private val repository = MSDLRepository.createRepository()
@Test
fun getHapticData_forAllHapticTokens_returnsCompositions() {
var validCompositions = 0
HapticToken.entries.forEach { token ->
if (repository.getHapticData(token)?.get() is HapticComposition) {
validCompositions++
}
}
assertThat(validCompositions).isEqualTo(HapticToken.entries.size)
}
}
@@ -0,0 +1,76 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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 com.google.android.msdl.domain
import android.os.VibrationAttributes
import android.os.VibrationEffect
import android.os.Vibrator
class FakeVibrator : Vibrator() {
var hasAmplitudeControl = true
var hasVibrator = true
var latestVibration: VibrationEffect? = null
private set
val supportedPrimitives =
mutableMapOf(
VibrationEffect.Composition.PRIMITIVE_CLICK to true,
VibrationEffect.Composition.PRIMITIVE_SPIN to true,
VibrationEffect.Composition.PRIMITIVE_THUD to true,
VibrationEffect.Composition.PRIMITIVE_TICK to true,
VibrationEffect.Composition.PRIMITIVE_LOW_TICK to true,
VibrationEffect.Composition.PRIMITIVE_QUICK_FALL to true,
VibrationEffect.Composition.PRIMITIVE_QUICK_RISE to true,
VibrationEffect.Composition.PRIMITIVE_SLOW_RISE to true,
)
var latestAttributes: VibrationAttributes? = null
private set
fun setSupportForAllPrimitives(supported: Boolean) =
supportedPrimitives.replaceAll { _, _ -> supported }
override fun cancel() {}
override fun cancel(usageFilter: Int) {}
override fun hasAmplitudeControl(): Boolean = this.hasAmplitudeControl
override fun hasVibrator(): Boolean = this.hasVibrator
override fun arePrimitivesSupported(vararg primitiveIds: Int): BooleanArray =
primitiveIds.map { id -> supportedPrimitives[id] ?: false }.toBooleanArray()
override fun vibrate(
uid: Int,
opPkg: String,
vibe: VibrationEffect,
reason: String,
attributes: VibrationAttributes,
) {
latestVibration = vibe
latestAttributes = attributes
}
override fun vibrate(
vibe: VibrationEffect,
attributes: VibrationAttributes,
) {
latestVibration = vibe
latestAttributes = attributes
}
}
@@ -0,0 +1,154 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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 com.google.android.msdl.domain
import android.os.VibrationAttributes
import android.os.VibrationEffect
import com.google.android.msdl.data.model.FeedbackLevel
import com.google.android.msdl.data.model.HapticComposition
import com.google.android.msdl.data.model.MSDLToken
import com.google.android.msdl.data.repository.MSDLRepository
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.Executor
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@RunWith(Parameterized::class)
class MSDLPlayerImplTest {
@Parameterized.Parameter lateinit var token: MSDLToken
private val repository = MSDLRepository.createRepository()
private val vibrator = FakeVibrator()
private val executor = Executor { it.run() }
private val useHapticFallbackForToken = MSDLToken.entries.associateWith { false }.toMutableMap()
private var msdlPlayer = MSDLPlayer.createPlayer(vibrator, executor, useHapticFallbackForToken)
@Before
fun setup() {
MSDLPlayer.SYSTEM_FEEDBACK_LEVEL = FeedbackLevel.EXPRESSIVE
vibrator.setSupportForAllPrimitives(true)
}
@Test
fun playToken_withExpressiveSetting_deliversAllFeedback() {
// GIVEN that the feedback level is expressive (all tokens play)
MSDLPlayer.SYSTEM_FEEDBACK_LEVEL = FeedbackLevel.EXPRESSIVE
val composition = repository.getHapticData(token.hapticToken)?.get() as? HapticComposition
val effect = composition?.composeIntoVibrationEffect()
// WHEN the token plays
msdlPlayer.playToken(token)
// THEN the vibration is delivered
assertThat(vibrator.latestVibration).isEqualTo(effect)
}
@Test
fun playToken_withoutFeedbackSetting_doesNotDeliverAnyFeedback() {
// GIVEN that the feedback level specifies no feedback
MSDLPlayer.SYSTEM_FEEDBACK_LEVEL = FeedbackLevel.NO_FEEDBACK
// WHEN the token plays
msdlPlayer.playToken(token)
// THEN no vibration is delivered
assertThat(vibrator.latestVibration).isNull()
}
@Test
fun playHapticComposition_withNullProperties_playsExpectedVibrationEffect() {
// GIVEN the vibration effect of a composition
val composition = repository.getHapticData(token.hapticToken)?.get() as? HapticComposition
val effect = composition?.composeIntoVibrationEffect()
// WHEN the composition is played for a token without interaction properties
msdlPlayer.playToken(token)
// THEN the vibration delivers the same vibration effect with USAGE_TOUCH vibration
// attributes and the correct token reason.
val touchAttributes =
VibrationAttributes.Builder().setUsage(VibrationAttributes.USAGE_TOUCH).build()
assertVibrationEffectDelivered(effect, touchAttributes)
}
@Test
fun playHapticComposition_withoutSupportedPrimitives_playsFallbackEffects() {
// GIVEN that no primitives are supported
useHapticFallbackForToken.replaceAll { _, _ -> true }
// GIVEN the fallback effect of a composition
val composition = repository.getHapticData(token.hapticToken)?.get() as? HapticComposition
val effect = composition?.fallbackEffect
// WHEN the composition is played for a token without interaction properties
msdlPlayer.playToken(token)
// THEN the vibration delivers the same fallback effect with USAGE_TOUCH vibration
val touchAttributes =
VibrationAttributes.Builder().setUsage(VibrationAttributes.USAGE_TOUCH).build()
assertVibrationEffectDelivered(effect, touchAttributes)
}
@Test
fun playHapticComposition_withDynamicVibrationScaleProperties_playsExpectedVibrationEffect() {
// GIVEN DynamicVibrationScaleProperties and a vibration effect built with this scale
val scaleOverride = 0.4f
val composition = repository.getHapticData(token.hapticToken)?.get() as? HapticComposition
val effect = composition?.composeIntoVibrationEffect(scaleOverride)
val attributes =
VibrationAttributes.Builder().setUsage(VibrationAttributes.USAGE_ALARM).build()
val properties = InteractionProperties.DynamicVibrationScale(scaleOverride, attributes)
// WHEN the composition is played for the token with the properties
msdlPlayer.playToken(token, properties)
// THEN the vibration effect delivered is the same vibration effect with the properties
assertVibrationEffectDelivered(effect, attributes)
}
@Test
fun playHapticComposition_withoutHardwareVibrator_doesNotPlayVibrationEffect() {
// GIVEN the vibration effect of a composition when there is no hardware vibrator
vibrator.hasVibrator = false
// WHEN the composition is played for a token
msdlPlayer.playToken(token)
// THEN the vibration does not deliver any effect
assertThat(vibrator.latestVibration).isNull()
}
private fun assertVibrationEffectDelivered(
effect: VibrationEffect?,
attributes: VibrationAttributes,
) {
assertThat(vibrator.latestVibration).isEqualTo(effect)
if (effect != null) {
assertThat(vibrator.latestAttributes).isEqualTo(attributes)
} else {
assertThat(vibrator.latestAttributes).isNull()
}
}
companion object {
@JvmStatic @Parameterized.Parameters fun tokens() = MSDLToken.entries
}
}
@@ -0,0 +1,108 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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 com.google.android.msdl.logging
import android.os.VibrationAttributes
import com.google.android.msdl.data.model.MSDLToken
import com.google.android.msdl.domain.InteractionProperties
import com.google.common.truth.Truth.assertThat
import kotlin.random.Random
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class MSDLHistoryLoggerImplTest {
private val properties =
object : InteractionProperties {
override val vibrationAttributes: VibrationAttributes =
VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK)
}
private val random = Random(0)
private val logger = MSDLHistoryLoggerImpl(MSDLHistoryLogger.HISTORY_SIZE)
@Test
fun addEvent_eventIsAddedToHistory() {
val token = getRandomToken()
val event = MSDLEvent(token, properties)
logger.addEvent(event)
assertThat(logger.getHistory().containsEvent(event)).isTrue()
}
@Test
fun addEvent_beyondLimit_keepsHistorySize() {
val tokens = getRandomTokens(MSDLHistoryLogger.HISTORY_SIZE + 10)
tokens.forEach { logger.addEvent(MSDLEvent(it, properties)) }
assertThat(logger.getHistory().size).isEqualTo(MSDLHistoryLogger.HISTORY_SIZE)
}
@Test
fun addEvent_beyondLimit_keepsLatestEvents() {
val localHistory = arrayListOf<MSDLEvent>()
val tokens = getRandomTokens(MSDLHistoryLogger.HISTORY_SIZE + 10)
var event: MSDLEvent
tokens.forEach {
event = MSDLEvent(it, properties)
logger.addEvent(event)
localHistory.add(event)
}
val latestLocalHistory = localHistory.takeLast(MSDLHistoryLogger.HISTORY_SIZE)
val loggerHistory = logger.getHistory()
assertThat(latestLocalHistory.isEqualTo(loggerHistory)).isTrue()
}
private fun getRandomToken(): MSDLToken =
MSDLToken.entries[random.nextInt(0, MSDLToken.entries.size)]
private fun getRandomTokens(n: Int): List<MSDLToken> = List(n) { getRandomToken() }
/**
* Check if the list is equal to another by making sure it has the same elements in the same
* order. Events are compared by their token name and interaction properties.
*
* @param[other] The other list to compare to.
*/
private fun List<MSDLEvent>.isEqualTo(other: List<MSDLEvent>): Boolean {
assert(other.size == this.size) { "Both lists must be of the same size" }
this.forEachIndexed { i, event ->
if (event.tokenName != other[i].tokenName || event.properties != other[i].properties) {
return false
}
}
return true
}
/**
* Check if the list contains an event. Events are compared by their token name and interaction
* properties.
*
* @param[other] The event to find.
*/
private fun List<MSDLEvent>.containsEvent(other: MSDLEvent): Boolean {
this.forEach { event ->
if (event.tokenName == other.tokenName && event.properties == other.properties) {
return true
}
}
return false
}
}
+3 -3
View File
@@ -474,15 +474,15 @@
<dimen name="bubblebar_overflow_inset">16dp</dimen> <dimen name="bubblebar_overflow_inset">16dp</dimen>
<dimen name="bubblebar_icon_spacing">6dp</dimen> <dimen name="bubblebar_icon_spacing">6dp</dimen>
<dimen name="bubblebar_icon_spacing_large">8dp</dimen> <dimen name="bubblebar_icon_spacing_large">8dp</dimen>
<dimen name="bubblebar_icon_spacing_persistent_taskbar">@dimen/bubblebar_icon_spacing</dimen> <dimen name="bubblebar_icon_spacing_persistent_taskbar">6dp</dimen>
<dimen name="bubblebar_expanded_icon_spacing">12dp</dimen> <dimen name="bubblebar_expanded_icon_spacing">12dp</dimen>
<dimen name="bubblebar_icon_elevation">1dp</dimen> <dimen name="bubblebar_icon_elevation">1dp</dimen>
<dimen name="bubblebar_transient_taskbar_min_distance">12dp</dimen> <dimen name="bubblebar_transient_taskbar_min_distance">12dp</dimen>
<!-- Bubble bar dismiss view --> <!-- Bubble bar dismiss view -->
<dimen name="bubblebar_dismiss_target_size">@dimen/floating_dismiss_background_size</dimen> <dimen name="bubblebar_dismiss_target_size">96dp</dimen>
<dimen name="bubblebar_dismiss_target_small_size">60dp</dimen> <dimen name="bubblebar_dismiss_target_small_size">60dp</dimen>
<dimen name="bubblebar_dismiss_target_icon_size">@dimen/floating_dismiss_icon_size</dimen> <dimen name="bubblebar_dismiss_target_icon_size">32dp</dimen>
<dimen name="bubblebar_dismiss_target_bottom_margin">50dp</dimen> <dimen name="bubblebar_dismiss_target_bottom_margin">50dp</dimen>
<dimen name="bubblebar_dismiss_floating_gradient_height">548dp</dimen> <dimen name="bubblebar_dismiss_floating_gradient_height">548dp</dimen>
<dimen name="bubblebar_dismiss_zone_width">192dp</dimen> <dimen name="bubblebar_dismiss_zone_width">192dp</dimen>
+4
View File
@@ -1,5 +1,9 @@
include ':IconLoader' include ':IconLoader'
project(':IconLoader').projectDir = new File(rootDir, 'iconloaderlib') project(':IconLoader').projectDir = new File(rootDir, 'iconloaderlib')
include ':Shared'
project(':Shared').projectDir = new File(rootDir, 'shared')
include ':msdllib'
project(':msdllib').projectDir = new File(rootDir, 'msdllib')
//include ':SharedLibWrapper' //include ':SharedLibWrapper'
//project(':SharedLibWrapper').projectDir = new File(rootDir, 'SharedLibWrapper') //project(':SharedLibWrapper').projectDir = new File(rootDir, 'SharedLibWrapper')
+33
View File
@@ -0,0 +1,33 @@
plugins {
id("com.android.library")
id("kotlin-android")
}
android {
namespace = "com.android.launcher3.testing.shared"
compileSdk = 36
defaultConfig {
minSdk = 26
targetSdk = 36
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
}
sourceSets {
named("main") {
java.setSrcDirs(listOf("src"))
manifest.srcFile("AndroidManifest.xml")
res.setSrcDirs(listOf("res"))
}
}
kotlin {
jvmToolchain(21)//1.8(8)
}
}
dependencies {
implementation("androidx.core:core:1.16.0")
implementation("androidx.annotation:annotation:1.9.1")
}
+9
View File
@@ -0,0 +1,9 @@
package android.appwidget.flags
object Flags {
@JvmStatic
fun generatedPreviews(): Boolean {
return true
}
}
+11
View File
@@ -0,0 +1,11 @@
package android.multiuser
object Flags {
@JvmStatic
fun enableMovingContentIntoPrivateSpace(): Boolean {
return true
}
}
+9
View File
@@ -0,0 +1,9 @@
package com.android.systemui.shared
object Flags {
@JvmStatic
fun newCustomizationPickerUi(): Boolean {
return true
}
}
+24
View File
@@ -0,0 +1,24 @@
package com.android.window.flags
object Flags {
@JvmStatic
fun predictiveBackThreeButtonNav(): Boolean {
return true
}
@JvmStatic
fun enableTaskbarConnectedDisplays(): Boolean {
return true
}
@JvmStatic
fun enableDesktopWindowingMode(): Boolean {
return true
}
@JvmStatic
fun enableDesktopWindowingWallpaperActivity(): Boolean {
return true
}
}
+45
View File
@@ -0,0 +1,45 @@
package com.android.wm.shell
object Flags {
@JvmStatic
fun enableRetrievableBubbles(): Boolean {
return true
}
@JvmStatic
fun enableTaskbarNavbarUnification(): Boolean {
return true;
}
@JvmStatic
fun enableGridOnlyOverview(): Boolean {
return true;
}
@JvmStatic
fun enableTaskbarOnPhones(): Boolean {
return true;
}
@JvmStatic
fun enableGsf(): Boolean {
return true;
}
@JvmStatic
fun enableBubbleBar(): Boolean {
return true;
}
@JvmStatic
fun enableBubbleBarOnPhones(): Boolean {
return true;
}
@JvmStatic
fun enableTinyTaskbar(): Boolean {
return true;
}
}