1 /*
<lambda>null2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.google.android.msdl.domain
18
19 import android.os.Build
20 import android.os.VibrationAttributes
21 import android.os.VibrationEffect
22 import android.os.Vibrator
23 import com.google.android.msdl.data.model.FeedbackLevel
24 import com.google.android.msdl.data.model.HapticComposition
25 import com.google.android.msdl.data.model.MSDLToken
26 import com.google.android.msdl.data.repository.MSDLRepository
27 import com.google.android.msdl.logging.MSDLEvent
28 import com.google.android.msdl.logging.MSDLHistoryLogger
29 import com.google.android.msdl.logging.MSDLHistoryLoggerImpl
30 import java.util.concurrent.Executor
31
32 /**
33 * Implementation of the MSDLPlayer.
34 *
35 * At the core, the player is in charge of delivering haptic and audio feedback closely in time.
36 *
37 * @param[repository] Repository to retrieve audio and haptic data.
38 * @param[executor] An [Executor] used to schedule haptic playback.
39 * @param[vibrator] Instance of the default [Vibrator] on the device.
40 * @param[useHapticFallbackForToken] A map that determines if the haptic fallback effect should be
41 * used for a given token.
42 */
43 internal class MSDLPlayerImpl(
44 private val repository: MSDLRepository,
45 private val vibrator: Vibrator,
46 private val executor: Executor,
47 private val useHapticFallbackForToken: Map<MSDLToken, Boolean?>,
48 ) : MSDLPlayer {
49
50 /** A logger to keep a history of playback events */
51 private val historyLogger = MSDLHistoryLoggerImpl(MSDLHistoryLogger.HISTORY_SIZE)
52
53 // TODO(b/355230334): This should be retrieved from the system Settings
54 override fun getSystemFeedbackLevel(): FeedbackLevel = MSDLPlayer.SYSTEM_FEEDBACK_LEVEL
55
56 override fun playToken(token: MSDLToken, properties: InteractionProperties?) {
57 // Don't play the data for the token if the current feedback level is below the minimal
58 // level of the token
59 if (getSystemFeedbackLevel() < token.minimumFeedbackLevel) return
60
61 // Play the data for the token with the given properties
62 playData(token, properties)
63 }
64
65 private fun playData(token: MSDLToken, properties: InteractionProperties?) {
66 // Gather the data from the repositories
67 val hapticData = repository.getHapticData(token.hapticToken)
68 val soundData = repository.getAudioData(token.soundToken)
69
70 // Nothing to play
71 if (hapticData == null && soundData == null) return
72
73 if (soundData == null) {
74 // Play haptics only
75 // 1. Create the effect
76 val composition: HapticComposition? = hapticData?.get() as? HapticComposition
77 val effect =
78 if (useHapticFallbackForToken[token] == true) {
79 composition?.fallbackEffect
80 } else {
81 when (properties) {
82 is InteractionProperties.DynamicVibrationScale -> {
83 composition?.composeIntoVibrationEffect(
84 scaleOverride = properties.scale
85 )
86 }
87 else -> composition?.composeIntoVibrationEffect() // compose as-is
88 }
89 }
90
91 // 2. Deliver the haptics with or without attributes
92 if (effect == null || !vibrator.hasVibrator()) return
93 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
94 val attributes =
95 if (properties?.vibrationAttributes != null) {
96 properties.vibrationAttributes
97 } else {
98 VibrationAttributes.Builder()
99 .setUsage(VibrationAttributes.USAGE_TOUCH)
100 .build()
101 }
102 executor.execute { vibrator.vibrate(effect, attributes) }
103 } else {
104 executor.execute { vibrator.vibrate(effect) }
105 }
106
107 // 3. Log the event
108 historyLogger.addEvent(MSDLEvent(token, properties))
109 } else {
110 // TODO(b/345248875): Play audio and haptics
111 }
112 }
113
114 override fun getHistory(): List<MSDLEvent> = historyLogger.getHistory()
115
116 override fun toString(): String =
117 """
118 Default MSDL player implementation.
119 Vibrator: $vibrator
120 Repository: $repository
121 """
122 .trimIndent()
123
124 companion object {
125 val REQUIRED_PRIMITIVES =
126 listOf(
127 VibrationEffect.Composition.PRIMITIVE_SPIN,
128 VibrationEffect.Composition.PRIMITIVE_THUD,
129 VibrationEffect.Composition.PRIMITIVE_TICK,
130 VibrationEffect.Composition.PRIMITIVE_CLICK,
131 VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
132 )
133 }
134 }
135
HapticCompositionnull136 fun HapticComposition.composeIntoVibrationEffect(
137 scaleOverride: Float? = null,
138 delayOverride: Int? = null,
139 ): VibrationEffect? {
140 val effectComposition = VibrationEffect.startComposition()
141 primitives.forEach { primitive ->
142 effectComposition.addPrimitive(
143 primitive.primitiveId,
144 scaleOverride ?: primitive.scale,
145 delayOverride ?: primitive.delayMillis,
146 )
147 }
148 return effectComposition.compose()
149 }
150