• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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