/* * Copyright (C) 2014 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.android.talkback.controller; import android.annotation.TargetApi; import com.android.talkback.EarconsPlayTask; import com.android.talkback.R; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.content.res.Resources; import android.content.res.Resources.NotFoundException; import android.media.AudioAttributes; import android.media.AudioManager; import android.media.SoundPool; import android.media.SoundPool.OnLoadCompleteListener; import android.os.Build; import android.os.Vibrator; import android.util.Log; import android.util.SparseIntArray; import com.android.talkback.TalkBackUpdateHelper; import com.android.utils.LogUtils; import com.android.utils.PackageManagerUtils; import com.android.utils.SecureSettingsUtils; import com.android.utils.SharedPreferencesUtils; import java.util.HashSet; import java.util.Set; /** * A feedback controller that caches sounds for quicker playback. */ public class FeedbackControllerApp implements FeedbackController { /** Maximum number of concurrent audio streams. */ private static final int MAX_STREAMS = 10; /** Default stream for audio feedback. */ private static final int DEFAULT_STREAM = AudioManager.STREAM_MUSIC; /** The parent context. */ private final Context mContext; /** The resources for this context. */ private final Resources mResources; /** The SoundPool instance for loading sounds and playing previously loaded sounds. */ private final SoundPool mSoundPool; /** The vibration service used to play vibration patterns. */ private final Vibrator mVibrator; /** Map from the resource IDs of loaded sounds to SoundPool sound IDs. */ private final SparseIntArray mSoundIds = new SparseIntArray(); /** The volume adjustment for sound feedback. */ private float mVolumeAdjustment = 1.0f; private boolean mAuditoryEnabled; private boolean mHapticEnabled; private final boolean mUseCompatKickBack; private final boolean mUseCompatSoundBack; private final Set<HapticFeedbackListener> mHapticFeedbackListeners = new HashSet<>(); // Due to a "feature" in SharedPreferences, this must be a member variable @SuppressWarnings("FieldCanBeLocal") private final OnSharedPreferenceChangeListener prefListener; @SuppressWarnings("deprecation") public FeedbackControllerApp(Context context) { mContext = context; mResources = context.getResources(); if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT_WATCH) { mSoundPool = createSoundPoolApi21(); } else { mSoundPool = new SoundPool(MAX_STREAMS, DEFAULT_STREAM, 0); } mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); // TODO: Do we really need to check compatibility on versions >ICS? mUseCompatKickBack = shouldUseCompatKickBack(); mUseCompatSoundBack = shouldUseCompatSoundBack(); prefListener = new OnSharedPreferenceChangeListener() { @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) { updatePreferences(sharedPreferences, s); } }; final SharedPreferences prefs = SharedPreferencesUtils.getSharedPreferences(context); prefs.registerOnSharedPreferenceChangeListener(prefListener); updatePreferences(prefs, null); } @TargetApi(21) private SoundPool createSoundPoolApi21() { AudioAttributes aa = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build(); return new SoundPool.Builder() .setMaxStreams(MAX_STREAMS) .setAudioAttributes(aa) .build(); } /** * @param enabled Whether haptic feedback should be enabled. */ private void setHapticEnabled(boolean enabled) { mHapticEnabled = enabled; } /** * @param enabled Whether auditory feedback should be enabled. */ private void setAuditoryEnabled(boolean enabled) { mAuditoryEnabled = enabled; } /** * Sets the current volume adjustment for auditory feedback. * * @param adjustment The amount by which to adjust the volume of auditory feedback. 0.0 mutes * the feedback while 1.0 plays it at its original volume. */ private void setVolumeAdjustment(float adjustment) { mVolumeAdjustment = adjustment; } @Override public boolean playHaptic(int resId) { if (!mHapticEnabled || resId == 0) { return false; } final int[] patternArray; try { patternArray = mResources.getIntArray(resId); } catch (NotFoundException e) { LogUtils.log(this, Log.ERROR, "Failed to load pattern %d", resId); return false; } final long[] pattern = new long[patternArray.length]; for (int i = 0; i < patternArray.length; i++) { pattern[i] = patternArray[i]; } long nanoTime = System.nanoTime(); for (HapticFeedbackListener listener : mHapticFeedbackListeners) { listener.onHapticFeedbackStarting(nanoTime); } mVibrator.vibrate(pattern, -1); return true; } @Override public void addHapticFeedbackListener(HapticFeedbackListener listener) { mHapticFeedbackListeners.add(listener); } @Override public void removeHapticFeedbackListener(HapticFeedbackListener listener) { mHapticFeedbackListeners.remove(listener); } @Override public void playAuditory(int resId) { playAuditory(resId, 1.0f /* rate */, 1.0f /* volume */); } @Override public void playAuditory(int resId, final float rate, float volume) { if (!mAuditoryEnabled || resId == 0) return; final float adjustedVolume = volume * mVolumeAdjustment; int soundId = mSoundIds.get(resId); if (soundId != 0) { new EarconsPlayTask(mSoundPool, soundId, adjustedVolume, rate).execute(); } else { // The sound could not be played from the cache. Start loading the sound into the // SoundPool for future use, and use a listener to play the sound ASAP. mSoundPool.setOnLoadCompleteListener(new OnLoadCompleteListener() { @Override public void onLoadComplete(SoundPool soundPool, int sampleId, int status) { if(sampleId !=0) { new EarconsPlayTask(mSoundPool, sampleId, adjustedVolume, rate).execute(); } } }); mSoundIds.put(resId, mSoundPool.load(mContext, resId, 1)); } } @Override public void interrupt() { // TODO: Stop all sounds. mVibrator.cancel(); } @Override public void shutdown() { mHapticFeedbackListeners.clear(); mSoundPool.release(); mVibrator.cancel(); } /** * Updates preferences from an instance of {@link SharedPreferences}. * Optionally specify a key to update only that preference. * * @param prefs An instance of {@link SharedPreferences}. * @param key The key to update, or {@code null} to update all preferences. */ private void updatePreferences(SharedPreferences prefs, String key) { if (key == null) { updateHapticFromPreference(prefs); updateAuditoryFromPreference(prefs); updateVolumeAdjustmentFromPreference(prefs); } else if (key.equals(mContext.getString(R.string.pref_vibration_key))) { updateHapticFromPreference(prefs); } else if (key.equals(mContext.getString(R.string.pref_soundback_key))) { updateAuditoryFromPreference(prefs); } else if (key.equals(mContext.getString(R.string.pref_soundback_volume_key))) { updateVolumeAdjustmentFromPreference(prefs); } } private void updateVolumeAdjustmentFromPreference(SharedPreferences prefs) { final int adjustment = SharedPreferencesUtils.getIntFromStringPref(prefs, mResources, R.string.pref_soundback_volume_key, R.string.pref_soundback_volume_default); setVolumeAdjustment(adjustment / 100.0f); } private void updateHapticFromPreference(SharedPreferences prefs) { final boolean enabled; if (mUseCompatKickBack) { enabled = SecureSettingsUtils.isAccessibilityServiceEnabled( mContext, TalkBackUpdateHelper.KICKBACK_PACKAGE); } else { enabled = SharedPreferencesUtils.getBooleanPref( prefs, mResources, R.string.pref_vibration_key, R.bool.pref_vibration_default); } setHapticEnabled(enabled); } private void updateAuditoryFromPreference(SharedPreferences prefs) { final boolean enabled; if (mUseCompatSoundBack) { enabled = SecureSettingsUtils.isAccessibilityServiceEnabled( mContext, TalkBackUpdateHelper.SOUNDBACK_PACKAGE); } else { enabled = SharedPreferencesUtils.getBooleanPref( prefs, mResources, R.string.pref_soundback_key, R.bool.pref_soundback_default); } setAuditoryEnabled(enabled); } private boolean shouldUseCompatKickBack() { if (!PackageManagerUtils.hasPackage(mContext, TalkBackUpdateHelper.KICKBACK_PACKAGE)) { return false; } final int kickBackVersionCode = PackageManagerUtils.getVersionCode( mContext, TalkBackUpdateHelper.KICKBACK_PACKAGE); return kickBackVersionCode >= TalkBackUpdateHelper.KICKBACK_REQUIRED_VERSION; } private boolean shouldUseCompatSoundBack() { if (!PackageManagerUtils.hasPackage(mContext, TalkBackUpdateHelper.SOUNDBACK_PACKAGE)) { return false; } final int kickBackVersionCode = PackageManagerUtils.getVersionCode( mContext, TalkBackUpdateHelper.SOUNDBACK_PACKAGE); return kickBackVersionCode >= TalkBackUpdateHelper.SOUNDBACK_REQUIRED_VERSION; } }