1 /* 2 * Copyright 2014, 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.server.telecom; 18 19 import android.content.Context; 20 import android.content.Intent; 21 import android.media.AudioAttributes; 22 import android.media.session.MediaSession; 23 import android.os.Handler; 24 import android.os.Looper; 25 import android.os.Message; 26 import android.telecom.Log; 27 import android.view.KeyEvent; 28 29 import com.android.internal.annotations.VisibleForTesting; 30 31 /** 32 * Static class to handle listening to the headset media buttons. 33 */ 34 public class HeadsetMediaButton extends CallsManagerListenerBase { 35 36 // Types of media button presses 37 @VisibleForTesting 38 public static final int SHORT_PRESS = 1; 39 @VisibleForTesting 40 public static final int LONG_PRESS = 2; 41 42 private static final AudioAttributes AUDIO_ATTRIBUTES = new AudioAttributes.Builder() 43 .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) 44 .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION).build(); 45 46 private static final int MSG_MEDIA_SESSION_INITIALIZE = 0; 47 private static final int MSG_MEDIA_SESSION_SET_ACTIVE = 1; 48 49 /** 50 * Wrapper class that abstracts an instance of {@link MediaSession} to the 51 * {@link MediaSessionAdapter} interface this class uses. This is done because 52 * {@link MediaSession} is a final class and cannot be mocked for testing purposes. 53 */ 54 public class MediaSessionWrapper implements MediaSessionAdapter { 55 private final MediaSession mMediaSession; 56 MediaSessionWrapper(MediaSession mediaSession)57 public MediaSessionWrapper(MediaSession mediaSession) { 58 mMediaSession = mediaSession; 59 } 60 61 /** 62 * Sets the underlying {@link MediaSession} active status. 63 * @param active 64 */ 65 @Override setActive(boolean active)66 public void setActive(boolean active) { 67 mMediaSession.setActive(active); 68 } 69 70 @Override setCallback(MediaSession.Callback callback)71 public void setCallback(MediaSession.Callback callback) { 72 mMediaSession.setCallback(callback); 73 } 74 75 /** 76 * Gets the underlying {@link MediaSession} active status. 77 * @return {@code true} if active, {@code false} otherwise. 78 */ 79 @Override isActive()80 public boolean isActive() { 81 return mMediaSession.isActive(); 82 } 83 } 84 85 /** 86 * Interface which defines the basic functionality of a {@link MediaSession} which is important 87 * for the {@link HeadsetMediaButton} to operator; this is for testing purposes so we can mock 88 * out that functionality. 89 */ 90 public interface MediaSessionAdapter { setActive(boolean active)91 void setActive(boolean active); setCallback(MediaSession.Callback callback)92 void setCallback(MediaSession.Callback callback); isActive()93 boolean isActive(); 94 } 95 96 private final MediaSession.Callback mSessionCallback = new MediaSession.Callback() { 97 @Override 98 public boolean onMediaButtonEvent(Intent intent) { 99 try { 100 Log.startSession("HMB.oMBE"); 101 KeyEvent event = (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); 102 Log.v(this, "SessionCallback.onMediaButton()... event = %s.", event); 103 if ((event != null) && ((event.getKeyCode() == KeyEvent.KEYCODE_HEADSETHOOK) || 104 (event.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))) { 105 synchronized (mLock) { 106 Log.v(this, "SessionCallback: HEADSETHOOK/MEDIA_PLAY_PAUSE"); 107 boolean consumed = handleCallMediaButton(event); 108 Log.v(this, "==> handleCallMediaButton(): consumed = %b.", consumed); 109 return consumed; 110 } 111 } 112 return true; 113 } finally { 114 Log.endSession(); 115 } 116 } 117 }; 118 119 private final Handler mMediaSessionHandler = new Handler(Looper.getMainLooper()) { 120 @Override 121 public void handleMessage(Message msg) { 122 switch (msg.what) { 123 case MSG_MEDIA_SESSION_INITIALIZE: { 124 MediaSession session = new MediaSession( 125 mContext, 126 HeadsetMediaButton.class.getSimpleName()); 127 session.setCallback(mSessionCallback); 128 session.setFlags(MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY 129 | MediaSession.FLAG_HANDLES_MEDIA_BUTTONS); 130 session.setPlaybackToLocal(AUDIO_ATTRIBUTES); 131 mSession = new MediaSessionWrapper(session); 132 break; 133 } 134 case MSG_MEDIA_SESSION_SET_ACTIVE: { 135 if (mSession != null) { 136 boolean activate = msg.arg1 != 0; 137 if (activate != mSession.isActive()) { 138 mSession.setActive(activate); 139 } 140 } 141 break; 142 } 143 default: 144 break; 145 } 146 } 147 }; 148 149 private final Context mContext; 150 private final CallsManager mCallsManager; 151 private final TelecomSystem.SyncRoot mLock; 152 private MediaSessionAdapter mSession; 153 private KeyEvent mLastHookEvent; 154 155 /** 156 * Constructor used for testing purposes to initialize a {@link HeadsetMediaButton} with a 157 * specified {@link MediaSessionAdapter}. Will not trigger MSG_MEDIA_SESSION_INITIALIZE and 158 * cause an actual {@link MediaSession} instance to be created. 159 * @param context the context 160 * @param callsManager the mock calls manager 161 * @param lock the lock 162 * @param adapter the adapter 163 */ 164 @VisibleForTesting HeadsetMediaButton( Context context, CallsManager callsManager, TelecomSystem.SyncRoot lock, MediaSessionAdapter adapter)165 public HeadsetMediaButton( 166 Context context, 167 CallsManager callsManager, 168 TelecomSystem.SyncRoot lock, 169 MediaSessionAdapter adapter) { 170 mContext = context; 171 mCallsManager = callsManager; 172 mLock = lock; 173 mSession = adapter; 174 175 adapter.setCallback(mSessionCallback); 176 } 177 178 /** 179 * Production code constructor; this version triggers MSG_MEDIA_SESSION_INITIALIZE which will 180 * create an actual instance of {@link MediaSession}. 181 * @param context the context 182 * @param callsManager the calls manager 183 * @param lock the telecom lock 184 */ HeadsetMediaButton( Context context, CallsManager callsManager, TelecomSystem.SyncRoot lock)185 public HeadsetMediaButton( 186 Context context, 187 CallsManager callsManager, 188 TelecomSystem.SyncRoot lock) { 189 mContext = context; 190 mCallsManager = callsManager; 191 mLock = lock; 192 193 // Create a MediaSession but don't enable it yet. This is a 194 // replacement for MediaButtonReceiver 195 mMediaSessionHandler.obtainMessage(MSG_MEDIA_SESSION_INITIALIZE).sendToTarget(); 196 } 197 198 /** 199 * Handles the wired headset button while in-call. 200 * 201 * @return true if we consumed the event. 202 */ handleCallMediaButton(KeyEvent event)203 private boolean handleCallMediaButton(KeyEvent event) { 204 Log.d(this, "handleCallMediaButton()...%s %s", event.getAction(), event.getRepeatCount()); 205 206 // Save ACTION_DOWN Event temporarily. 207 if (event.getAction() == KeyEvent.ACTION_DOWN) { 208 mLastHookEvent = event; 209 } 210 211 if (event.isLongPress()) { 212 return mCallsManager.onMediaButton(LONG_PRESS); 213 } else if (event.getAction() == KeyEvent.ACTION_UP) { 214 // We should not judge SHORT_PRESS by ACTION_UP event repeatCount, because it always 215 // return 0. 216 // Actually ACTION_DOWN event repeatCount only increases when LONG_PRESS performed. 217 if (mLastHookEvent != null && mLastHookEvent.getRepeatCount() == 0) { 218 return mCallsManager.onMediaButton(SHORT_PRESS); 219 } 220 } 221 222 if (event.getAction() != KeyEvent.ACTION_DOWN) { 223 mLastHookEvent = null; 224 } 225 226 return true; 227 } 228 229 /** ${inheritDoc} */ 230 @Override onCallAdded(Call call)231 public void onCallAdded(Call call) { 232 if (call.isExternalCall()) { 233 return; 234 } 235 handleCallAddition(); 236 } 237 238 /** 239 * Triggers session activation due to call addition. 240 */ handleCallAddition()241 private void handleCallAddition() { 242 mMediaSessionHandler.obtainMessage(MSG_MEDIA_SESSION_SET_ACTIVE, 1, 0).sendToTarget(); 243 } 244 245 /** ${inheritDoc} */ 246 @Override onCallRemoved(Call call)247 public void onCallRemoved(Call call) { 248 if (call.isExternalCall()) { 249 return; 250 } 251 handleCallRemoval(); 252 } 253 254 /** 255 * Triggers session deactivation due to call removal. 256 */ handleCallRemoval()257 private void handleCallRemoval() { 258 if (!mCallsManager.hasAnyCalls()) { 259 mMediaSessionHandler.obtainMessage(MSG_MEDIA_SESSION_SET_ACTIVE, 0, 0).sendToTarget(); 260 } 261 } 262 263 /** ${inheritDoc} */ 264 @Override onExternalCallChanged(Call call, boolean isExternalCall)265 public void onExternalCallChanged(Call call, boolean isExternalCall) { 266 // Note: We don't use the onCallAdded/onCallRemoved methods here since they do checks to see 267 // if the call is external or not and would skip the session activation/deactivation. 268 if (isExternalCall) { 269 handleCallRemoval(); 270 } else { 271 handleCallAddition(); 272 } 273 } 274 275 @VisibleForTesting 276 /** 277 * @return the handler this class instance uses for operation; used for unit testing. 278 */ getHandler()279 public Handler getHandler() { 280 return mMediaSessionHandler; 281 } 282 } 283