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