1 /* 2 * Copyright (C) 2016 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.incallui.ringtone; 18 19 import android.media.AudioManager; 20 import android.media.ToneGenerator; 21 import android.support.annotation.NonNull; 22 import android.support.annotation.Nullable; 23 import com.android.incallui.Log; 24 import com.android.incallui.async.PausableExecutor; 25 import java.util.Objects; 26 import java.util.concurrent.CountDownLatch; 27 import java.util.concurrent.TimeUnit; 28 29 /** 30 * Class responsible for playing in-call related tones in a background thread. This class only 31 * allows one tone to be played at a time. 32 */ 33 public class InCallTonePlayer { 34 35 public static final int TONE_CALL_WAITING = 4; 36 37 public static final int VOLUME_RELATIVE_HIGH_PRIORITY = 80; 38 39 @NonNull private final ToneGeneratorFactory toneGeneratorFactory; 40 @NonNull private final PausableExecutor executor; 41 private @Nullable CountDownLatch numPlayingTones; 42 43 /** 44 * Creates a new InCallTonePlayer. 45 * 46 * @param toneGeneratorFactory the {@link ToneGeneratorFactory} used to create {@link 47 * ToneGenerator}s. 48 * @param executor the {@link PausableExecutor} used to play tones in a background thread. 49 * @throws NullPointerException if audioModeProvider, toneGeneratorFactory, or executor are {@code 50 * null}. 51 */ InCallTonePlayer( @onNull ToneGeneratorFactory toneGeneratorFactory, @NonNull PausableExecutor executor)52 public InCallTonePlayer( 53 @NonNull ToneGeneratorFactory toneGeneratorFactory, @NonNull PausableExecutor executor) { 54 this.toneGeneratorFactory = Objects.requireNonNull(toneGeneratorFactory); 55 this.executor = Objects.requireNonNull(executor); 56 } 57 58 /** @return {@code true} if a tone is currently playing, {@code false} otherwise. */ isPlayingTone()59 public boolean isPlayingTone() { 60 return numPlayingTones != null && numPlayingTones.getCount() > 0; 61 } 62 63 /** 64 * Plays the given tone in a background thread. 65 * 66 * @param tone the tone to play. 67 * @throws IllegalStateException if a tone is already playing. 68 * @throws IllegalArgumentException if the tone is invalid. 69 */ play(int tone)70 public void play(int tone) { 71 if (isPlayingTone()) { 72 throw new IllegalStateException("Tone already playing"); 73 } 74 final ToneGeneratorInfo info = getToneGeneratorInfo(tone); 75 numPlayingTones = new CountDownLatch(1); 76 executor.execute( 77 new Runnable() { 78 @Override 79 public void run() { 80 playOnBackgroundThread(info); 81 } 82 }); 83 } 84 getToneGeneratorInfo(int tone)85 private ToneGeneratorInfo getToneGeneratorInfo(int tone) { 86 switch (tone) { 87 case TONE_CALL_WAITING: 88 /* 89 * DialerCall waiting tones play until they're stopped either by the user accepting or 90 * declining the call so the tone length is set at what's effectively forever. The 91 * tone is played at a high priority volume and through STREAM_VOICE_CALL since it's 92 * call related and using that stream will route it through bluetooth devices 93 * appropriately. 94 */ 95 return new ToneGeneratorInfo( 96 ToneGenerator.TONE_SUP_CALL_WAITING, 97 VOLUME_RELATIVE_HIGH_PRIORITY, 98 Integer.MAX_VALUE, 99 AudioManager.STREAM_VOICE_CALL); 100 default: 101 throw new IllegalArgumentException("Bad tone: " + tone); 102 } 103 } 104 playOnBackgroundThread(ToneGeneratorInfo info)105 private void playOnBackgroundThread(ToneGeneratorInfo info) { 106 ToneGenerator toneGenerator = null; 107 try { 108 Log.v(this, "Starting tone " + info); 109 toneGenerator = toneGeneratorFactory.newInCallToneGenerator(info.stream, info.volume); 110 toneGenerator.startTone(info.tone); 111 /* 112 * During tests, this will block until the tests call mExecutor.ackMilestone. This call 113 * allows for synchronization to the point where the tone has started playing. 114 */ 115 executor.milestone(); 116 if (numPlayingTones != null) { 117 numPlayingTones.await(info.toneLengthMillis, TimeUnit.MILLISECONDS); 118 // Allows for synchronization to the point where the tone has completed playing. 119 executor.milestone(); 120 } 121 } catch (InterruptedException e) { 122 Log.w(this, "Interrupted while playing in-call tone."); 123 } finally { 124 if (toneGenerator != null) { 125 toneGenerator.release(); 126 } 127 if (numPlayingTones != null) { 128 numPlayingTones.countDown(); 129 } 130 // Allows for synchronization to the point where this background thread has cleaned up. 131 executor.milestone(); 132 } 133 } 134 135 /** Stops playback of the current tone. */ stop()136 public void stop() { 137 if (numPlayingTones != null) { 138 numPlayingTones.countDown(); 139 } 140 } 141 142 private static class ToneGeneratorInfo { 143 144 public final int tone; 145 public final int volume; 146 public final int toneLengthMillis; 147 public final int stream; 148 ToneGeneratorInfo(int toneGeneratorType, int volume, int toneLengthMillis, int stream)149 public ToneGeneratorInfo(int toneGeneratorType, int volume, int toneLengthMillis, int stream) { 150 this.tone = toneGeneratorType; 151 this.volume = volume; 152 this.toneLengthMillis = toneLengthMillis; 153 this.stream = stream; 154 } 155 156 @Override toString()157 public String toString() { 158 return "ToneGeneratorInfo{" 159 + "toneLengthMillis=" 160 + toneLengthMillis 161 + ", tone=" 162 + tone 163 + ", volume=" 164 + volume 165 + '}'; 166 } 167 } 168 } 169