• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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.android.tv.settings;
18 
19 import android.annotation.IntDef;
20 import android.annotation.MainThread;
21 import android.content.Context;
22 import android.media.AudioAttributes;
23 import android.media.SoundPool;
24 import android.os.Handler;
25 import android.os.HandlerThread;
26 import android.os.Looper;
27 import android.os.Message;
28 import android.provider.Settings;
29 import android.util.Log;
30 
31 import androidx.annotation.NonNull;
32 import androidx.lifecycle.Lifecycle;
33 import androidx.lifecycle.LifecycleObserver;
34 import androidx.lifecycle.OnLifecycleEvent;
35 import androidx.lifecycle.ProcessLifecycleOwner;
36 
37 import java.lang.annotation.Retention;
38 import java.lang.annotation.RetentionPolicy;
39 import java.util.HashSet;
40 import java.util.Map;
41 import java.util.Set;
42 import java.util.concurrent.ConcurrentHashMap;
43 
44 /**
45  * System sounds player used to play system sounds like select / deselect.
46  * To keep the sound effects in memory only when the app is active, this class observes the
47  * {@link androidx.lifecycle.Lifecycle} of the {@link androidx.lifecycle.ProcessLifecycleOwner} and
48  * loads the sound effect when ON_START occurs and unloads them when ON_STOP occurs.
49  * To achieve a consistent volume among all system sounds the {@link SoundPool} used here is
50  * initialized in the same way as the SoundPool for framework system sounds in SoundEffectsHelper
51  * and the volume attenuation is calculated in the same way as it's done by SoundEffectsHelper.
52  */
53 public class SystemSoundsPlayer implements LifecycleObserver {
54     public static final int FX_SELECT = 0;
55     public static final int FX_DESELECT = 1;
56     /** @hide */
57     @IntDef(prefix = "FX_", value = {FX_SELECT, FX_DESELECT})
58     @Retention(RetentionPolicy.SOURCE)
59     public @interface SystemSoundEffect {}
60     private static final String TAG = SystemSoundsPlayer.class.getSimpleName();
61     private static final int NUM_SOUNDPOOL_STREAMS = 2;
62     private static final int MSG_PRELOAD_SOUNDS = 0;
63     private static final int MSG_UNLOAD_SOUNDS = 1;
64     private static final int MSG_PLAY_SOUND = 2;
65     private static final int[] FX_RESOURCES = new int[]{
66             R.raw.Select,
67             R.raw.Deselect
68     };
69     private final Handler mHandler;
70     private final Context mContext;
71     private final Map<Integer, Integer> mEffectIdToSoundPoolId = new ConcurrentHashMap<>();
72     private final Set<Integer> mLoadedSoundPoolIds = new HashSet<>();
73     private final float mVolumeAttenuation;
74     private SoundPool mSoundPool;
75 
SystemSoundsPlayer(Context context)76     public SystemSoundsPlayer(Context context) {
77         mContext = context.getApplicationContext();
78         float attenuationDb = mContext.getResources().getInteger(
79                 com.android.internal.R.integer.config_soundEffectVolumeDb);
80         // This is the same value that is used for framework system sounds as set by
81         // com.android.server.audio.SoundEffectsHelper#onPlaySoundEffect()
82         mVolumeAttenuation = (float) Math.pow(10, attenuationDb / 20);
83         HandlerThread handlerThread = new HandlerThread(TAG + ".handler");
84         handlerThread.start();
85         mHandler = new SoundPoolHandler(handlerThread.getLooper());
86         ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
87     }
88 
89     /**
90      * Plays a sound effect
91      *
92      * @param effect The effect id.
93      */
playSoundEffect(@ystemSoundEffect int effect)94     public void playSoundEffect(@SystemSoundEffect int effect) {
95         if (mSoundPool == null || !querySoundEffectsEnabled()) {
96             return;
97         }
98         switch (effect) {
99             case FX_SELECT:
100             case FX_DESELECT:
101                 // any other "case X:" in the future
102                 int soundPoolSoundId = getSoundPoolIdForEffect(effect);
103                 if (soundPoolSoundId >= 0) {
104                     mHandler.sendMessage(mHandler.obtainMessage(MSG_PLAY_SOUND, soundPoolSoundId, 0,
105                             mSoundPool));
106                 } else {
107                     Log.w(TAG, "playSoundEffect() called but SoundPool is not ready");
108                 }
109                 break;
110             default:
111                 Log.w(TAG, "Invalid sound id: " + effect);
112         }
113     }
114 
115     @OnLifecycleEvent(Lifecycle.Event.ON_START)
prepareSoundPool()116     private void prepareSoundPool() {
117         if (mSoundPool == null) {
118             mSoundPool = new SoundPool.Builder()
119                     .setMaxStreams(NUM_SOUNDPOOL_STREAMS)
120                     .setAudioAttributes(new AudioAttributes.Builder()
121                             .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
122                             .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
123                             .build())
124                     .build();
125             mSoundPool.setOnLoadCompleteListener(new SoundPoolLoadCompleteListener());
126             mHandler.sendMessage(mHandler.obtainMessage(MSG_PRELOAD_SOUNDS, mSoundPool));
127         } else {
128             throw new IllegalStateException("prepareSoundPool() was called but SoundPool not null");
129         }
130     }
131 
132     @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
releaseSoundPool()133     private void releaseSoundPool() {
134         if (mSoundPool == null) {
135             throw new IllegalStateException("releaseSoundPool() was called but SoundPool is null");
136         }
137         mHandler.sendMessage(mHandler.obtainMessage(MSG_UNLOAD_SOUNDS, mSoundPool));
138         mSoundPool.setOnLoadCompleteListener(null);
139         mSoundPool = null;
140         mLoadedSoundPoolIds.clear();
141     }
142 
143     /**
144      * Settings has an in memory cache, so this is fast.
145      */
querySoundEffectsEnabled()146     private boolean querySoundEffectsEnabled() {
147         return Settings.System.getIntForUser(mContext.getContentResolver(),
148                 Settings.System.SOUND_EFFECTS_ENABLED, 0, mContext.getUserId()) != 0;
149     }
150 
151     /**
152      * @param effect Any of the defined effect ids.
153      * @return Returns the SoundPool sound id if the sound has been loaded, -1 otherwise.
154      */
getSoundPoolIdForEffect(@ystemSoundEffect int effect)155     private int getSoundPoolIdForEffect(@SystemSoundEffect int effect) {
156         Integer soundPoolSoundId = mEffectIdToSoundPoolId.getOrDefault(effect, -1);
157         if (mLoadedSoundPoolIds.contains(soundPoolSoundId)) {
158             return soundPoolSoundId;
159         } else {
160             return -1;
161         }
162     }
163 
164     private class SoundPoolHandler extends Handler {
SoundPoolHandler(@onNull Looper looper)165         SoundPoolHandler(@NonNull Looper looper) {
166             super(looper);
167         }
168 
169         @Override
handleMessage(@onNull Message msg)170         public void handleMessage(@NonNull Message msg) {
171             SoundPool soundPool = (SoundPool) msg.obj;
172             switch (msg.what) {
173                 case MSG_PRELOAD_SOUNDS:
174                     for (int effectId = 0; effectId < FX_RESOURCES.length; effectId++) {
175                         int soundPoolSoundId = soundPool.load(mContext,
176                                 FX_RESOURCES[effectId], /* priority= */ 1);
177                         mEffectIdToSoundPoolId.put(effectId, soundPoolSoundId);
178                     }
179                     break;
180                 case MSG_UNLOAD_SOUNDS:
181                     mEffectIdToSoundPoolId.clear();
182                     soundPool.release();
183                     break;
184                 case MSG_PLAY_SOUND:
185                     int soundId = msg.arg1;
186                     soundPool.play(soundId, mVolumeAttenuation, mVolumeAttenuation, /* priority= */
187                             0, /* loop= */0, /* rate= */ 1.0f);
188                     break;
189             }
190         }
191     }
192 
193     private class SoundPoolLoadCompleteListener implements
194             SoundPool.OnLoadCompleteListener {
195         @MainThread
196         @Override
onLoadComplete(SoundPool soundPool, int sampleId, int status)197         public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
198             if (mSoundPool != soundPool) {
199                 // in case the soundPool has already been released we do not care
200                 return;
201             }
202             if (status == 0) {
203                 // sound loaded successfully
204                 mLoadedSoundPoolIds.add(sampleId);
205             } else {
206                 // error while loading sound, remove it from map to mark it as unloaded
207                 Integer effectId = 0;
208                 for (; effectId < mEffectIdToSoundPoolId.size(); effectId++) {
209                     if (mEffectIdToSoundPoolId.get(effectId) == sampleId) {
210                         break;
211                     }
212                 }
213                 mEffectIdToSoundPoolId.remove(effectId);
214             }
215             int remainingToLoad = mEffectIdToSoundPoolId.size() - mLoadedSoundPoolIds.size();
216             if (remainingToLoad == 0) {
217                 soundPool.setOnLoadCompleteListener(null);
218             }
219         }
220     }
221 }
222