Skip to content

Commit 4294b83

Browse files
committed
Start voice-based interactions from headsethook key
Implement the interception of long-press on keys that can be used to trigger voice-based interactions (here only HEADSETHOOK) only if the long-press hasn't been trapped by the foreground application: - if the key is not one that is used for voice input, handle the event as before. - if the key press wasn't long enough, simulate a delayed key press. - if long press is detected, send RecognizerIntent.ACTION_WEB_SEARCH. Long press duration is set to 300ms (a typical key press is under 100ms). Bug 3225090 Change-Id: I5b3adeb91d3c41ccd54d23fdb93d7eaec496eee7
1 parent c2e393f commit 4294b83

File tree

1 file changed

+220
-9
lines changed

1 file changed

+220
-9
lines changed

media/java/android/media/AudioService.java

Lines changed: 220 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import android.bluetooth.BluetoothDevice;
3434
import android.bluetooth.BluetoothHeadset;
3535
import android.bluetooth.BluetoothProfile;
36+
import android.content.ActivityNotFoundException;
3637
import android.content.BroadcastReceiver;
3738
import android.content.ComponentName;
3839
import android.content.ContentResolver;
@@ -3575,31 +3576,43 @@ public void unregisterAudioFocusClient(String clientId) {
35753576
// RemoteControl
35763577
//==========================================================================================
35773578
public void dispatchMediaKeyEvent(KeyEvent keyEvent) {
3578-
dispatchMediaKeyEvent(keyEvent, false /*needWakeLock*/);
3579+
filterMediaKeyEvent(keyEvent, false /*needWakeLock*/);
35793580
}
35803581

35813582
public void dispatchMediaKeyEventUnderWakelock(KeyEvent keyEvent) {
3582-
dispatchMediaKeyEvent(keyEvent, true /*needWakeLock*/);
3583+
filterMediaKeyEvent(keyEvent, true /*needWakeLock*/);
35833584
}
35843585

3585-
/**
3586-
* Handles the dispatching of the media button events to one of the registered listeners,
3587-
* or if there was none, broadcast a ACTION_MEDIA_BUTTON intent to the rest of the system.
3588-
*/
3589-
private void dispatchMediaKeyEvent(KeyEvent keyEvent, boolean needWakeLock) {
3586+
private void filterMediaKeyEvent(KeyEvent keyEvent, boolean needWakeLock) {
35903587
// sanity check on the incoming key event
35913588
if (!isValidMediaKeyEvent(keyEvent)) {
35923589
Log.e(TAG, "not dispatching invalid media key event " + keyEvent);
35933590
return;
35943591
}
3595-
// event filtering
3592+
// event filtering based on audio mode
35963593
synchronized(mRingingLock) {
35973594
if (mIsRinging || (getMode() == AudioSystem.MODE_IN_CALL) ||
35983595
(getMode() == AudioSystem.MODE_IN_COMMUNICATION) ||
35993596
(getMode() == AudioSystem.MODE_RINGTONE) ) {
36003597
return;
36013598
}
36023599
}
3600+
// event filtering based on voice-based interactions
3601+
if (isValidVoiceInputKeyCode(keyEvent.getKeyCode())) {
3602+
filterVoiceInputKeyEvent(keyEvent, needWakeLock);
3603+
} else {
3604+
dispatchMediaKeyEvent(keyEvent, needWakeLock);
3605+
}
3606+
}
3607+
3608+
/**
3609+
* Handles the dispatching of the media button events to one of the registered listeners,
3610+
* or if there was none, broadcast an ACTION_MEDIA_BUTTON intent to the rest of the system.
3611+
* @param keyEvent a non-null KeyEvent whose key code is one of the supported media buttons
3612+
* @param needWakeLock true if a PARTIAL_WAKE_LOCK needs to be held while this key event
3613+
* is dispatched.
3614+
*/
3615+
private void dispatchMediaKeyEvent(KeyEvent keyEvent, boolean needWakeLock) {
36033616
if (needWakeLock) {
36043617
mMediaEventWakeLock.acquire();
36053618
}
@@ -3628,6 +3641,140 @@ private void dispatchMediaKeyEvent(KeyEvent keyEvent, boolean needWakeLock) {
36283641
}
36293642
}
36303643

3644+
/**
3645+
* The minimum duration during which a user must press to trigger voice-based interactions
3646+
*/
3647+
private final static int MEDIABUTTON_LONG_PRESS_DURATION_MS = 300;
3648+
/**
3649+
* The different states of the state machine to handle the launch of voice-based interactions,
3650+
* stored in mVoiceButtonState.
3651+
*/
3652+
private final static int VOICEBUTTON_STATE_IDLE = 0;
3653+
private final static int VOICEBUTTON_STATE_DOWN = 1;
3654+
private final static int VOICEBUTTON_STATE_DOWN_IGNORE_NEW = 2;
3655+
/**
3656+
* The different actions after state transitions on mVoiceButtonState.
3657+
*/
3658+
private final static int VOICEBUTTON_ACTION_DISCARD_CURRENT_KEY_PRESS = 1;
3659+
private final static int VOICEBUTTON_ACTION_START_VOICE_INPUT = 2;
3660+
private final static int VOICEBUTTON_ACTION_SIMULATE_KEY_PRESS = 3;
3661+
3662+
private final Object mVoiceEventLock = new Object();
3663+
private int mVoiceButtonState = VOICEBUTTON_STATE_IDLE;
3664+
private long mVoiceButtonDownTime = 0;
3665+
3666+
/**
3667+
* Log an error when an unexpected action is encountered in the state machine to filter
3668+
* key events.
3669+
* @param keyAction the unexpected action of the key event being filtered
3670+
* @param stateName the string corresponding to the state in which the error occurred
3671+
*/
3672+
private static void logErrorForKeyAction(int keyAction, String stateName) {
3673+
Log.e(TAG, "unexpected action "
3674+
+ KeyEvent.actionToString(keyAction)
3675+
+ " in " + stateName + " state");
3676+
}
3677+
3678+
/**
3679+
* Filter key events that may be used for voice-based interactions
3680+
* @param keyEvent a non-null KeyEvent whose key code is that of one of the supported
3681+
* media buttons that can be used to trigger voice-based interactions.
3682+
* @param needWakeLock true if a PARTIAL_WAKE_LOCK needs to be held while this key event
3683+
* is dispatched.
3684+
*/
3685+
private void filterVoiceInputKeyEvent(KeyEvent keyEvent, boolean needWakeLock) {
3686+
int voiceButtonAction = VOICEBUTTON_ACTION_DISCARD_CURRENT_KEY_PRESS;
3687+
int keyAction = keyEvent.getAction();
3688+
synchronized (mVoiceEventLock) {
3689+
// state machine on mVoiceButtonState
3690+
switch (mVoiceButtonState) {
3691+
3692+
case VOICEBUTTON_STATE_IDLE:
3693+
if (keyAction == KeyEvent.ACTION_DOWN) {
3694+
mVoiceButtonDownTime = keyEvent.getDownTime();
3695+
// valid state transition
3696+
mVoiceButtonState = VOICEBUTTON_STATE_DOWN;
3697+
} else if (keyAction == KeyEvent.ACTION_UP) {
3698+
// no state transition
3699+
// action is still VOICEBUTTON_ACTION_DISCARD_CURRENT_KEY_PRESS
3700+
} else {
3701+
logErrorForKeyAction(keyAction, "VOICEBUTTON_STATE_IDLE");
3702+
}
3703+
break;
3704+
3705+
case VOICEBUTTON_STATE_DOWN:
3706+
if ((keyEvent.getEventTime() - mVoiceButtonDownTime)
3707+
>= MEDIABUTTON_LONG_PRESS_DURATION_MS) {
3708+
// press was long enough, start voice-based interactions, regardless of
3709+
// whether this was a DOWN or UP key event
3710+
voiceButtonAction = VOICEBUTTON_ACTION_START_VOICE_INPUT;
3711+
if (keyAction == KeyEvent.ACTION_UP) {
3712+
// done tracking the key press, so transition back to idle state
3713+
mVoiceButtonState = VOICEBUTTON_STATE_IDLE;
3714+
} else if (keyAction == KeyEvent.ACTION_DOWN) {
3715+
// no need to observe the upcoming key events
3716+
mVoiceButtonState = VOICEBUTTON_STATE_DOWN_IGNORE_NEW;
3717+
} else {
3718+
logErrorForKeyAction(keyAction, "VOICEBUTTON_STATE_DOWN");
3719+
}
3720+
} else {
3721+
if (keyAction == KeyEvent.ACTION_UP) {
3722+
// press wasn't long enough, simulate complete key press
3723+
voiceButtonAction = VOICEBUTTON_ACTION_SIMULATE_KEY_PRESS;
3724+
// not tracking the key press anymore, so transition back to idle state
3725+
mVoiceButtonState = VOICEBUTTON_STATE_IDLE;
3726+
} else if (keyAction == KeyEvent.ACTION_DOWN) {
3727+
// no state transition
3728+
// action is still VOICEBUTTON_ACTION_DISCARD_CURRENT_KEY_PRESS
3729+
} else {
3730+
logErrorForKeyAction(keyAction, "VOICEBUTTON_STATE_DOWN");
3731+
}
3732+
}
3733+
break;
3734+
3735+
case VOICEBUTTON_STATE_DOWN_IGNORE_NEW:
3736+
if (keyAction == KeyEvent.ACTION_UP) {
3737+
// done tracking the key press, so transition back to idle state
3738+
mVoiceButtonState = VOICEBUTTON_STATE_IDLE;
3739+
// action is still VOICEBUTTON_ACTION_DISCARD_CURRENT_KEY_PRESS
3740+
} else if (keyAction == KeyEvent.ACTION_DOWN) {
3741+
// no state transition: we've already launched voice-based interactions
3742+
// action is still VOICEBUTTON_ACTION_DISCARD_CURRENT_KEY_PRESS
3743+
} else {
3744+
logErrorForKeyAction(keyAction, "VOICEBUTTON_STATE_DOWN_IGNORE_NEW");
3745+
}
3746+
break;
3747+
}
3748+
}//synchronized (mVoiceEventLock)
3749+
3750+
// take action after media button event filtering for voice-based interactions
3751+
switch (voiceButtonAction) {
3752+
case VOICEBUTTON_ACTION_DISCARD_CURRENT_KEY_PRESS:
3753+
if (DEBUG_RC) Log.v(TAG, " ignore key event");
3754+
break;
3755+
case VOICEBUTTON_ACTION_START_VOICE_INPUT:
3756+
if (DEBUG_RC) Log.v(TAG, " start voice-based interactions");
3757+
// then start the voice-based interactions
3758+
startVoiceBasedInteractions(needWakeLock);
3759+
break;
3760+
case VOICEBUTTON_ACTION_SIMULATE_KEY_PRESS:
3761+
if (DEBUG_RC) Log.v(TAG, " send simulated key event");
3762+
sendSimulatedMediaButtonEvent(keyEvent, needWakeLock);
3763+
break;
3764+
}
3765+
}
3766+
3767+
private void sendSimulatedMediaButtonEvent(KeyEvent originalKeyEvent, boolean needWakeLock) {
3768+
// send DOWN event
3769+
KeyEvent keyEvent = KeyEvent.changeAction(originalKeyEvent, KeyEvent.ACTION_DOWN);
3770+
dispatchMediaKeyEvent(keyEvent, needWakeLock);
3771+
// send UP event
3772+
keyEvent = KeyEvent.changeAction(originalKeyEvent, KeyEvent.ACTION_UP);
3773+
dispatchMediaKeyEvent(keyEvent, needWakeLock);
3774+
3775+
}
3776+
3777+
36313778
private static boolean isValidMediaKeyEvent(KeyEvent keyEvent) {
36323779
if (keyEvent == null) {
36333780
return false;
@@ -3654,6 +3801,63 @@ private static boolean isValidMediaKeyEvent(KeyEvent keyEvent) {
36543801
return true;
36553802
}
36563803

3804+
/**
3805+
* Checks whether the given key code is one that can trigger the launch of voice-based
3806+
* interactions.
3807+
* @param keyCode the key code associated with the key event
3808+
* @return true if the key is one of the supported voice-based interaction triggers
3809+
*/
3810+
private static boolean isValidVoiceInputKeyCode(int keyCode) {
3811+
if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK) {
3812+
return true;
3813+
} else {
3814+
return false;
3815+
}
3816+
}
3817+
3818+
/**
3819+
* Tell the system to start voice-based interactions / voice commands
3820+
*/
3821+
private void startVoiceBasedInteractions(boolean needWakeLock) {
3822+
Intent voiceIntent = new Intent(android.speech.RecognizerIntent.ACTION_WEB_SEARCH);
3823+
if (needWakeLock) {
3824+
mMediaEventWakeLock.acquire();
3825+
}
3826+
voiceIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
3827+
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
3828+
try {
3829+
if (mKeyguardManager != null) {
3830+
// it's ok to start voice-based interactions when:
3831+
// - the device is locked but doesn't require a password to be unlocked
3832+
// - the device is not locked
3833+
if ((mKeyguardManager.isKeyguardLocked() && !mKeyguardManager.isKeyguardSecure())
3834+
|| !mKeyguardManager.isKeyguardLocked()) {
3835+
mContext.startActivity(voiceIntent);
3836+
}
3837+
}
3838+
} catch (ActivityNotFoundException e) {
3839+
Log.e(TAG, "Error launching activity for ACTION_WEB_SEARCH: " + e);
3840+
} finally {
3841+
if (needWakeLock) {
3842+
mMediaEventWakeLock.release();
3843+
}
3844+
}
3845+
}
3846+
3847+
/**
3848+
* Verify whether it is safe to start voice-based interactions given the state of the system
3849+
* @return false is the Keyguard is locked and secure, true otherwise
3850+
*/
3851+
private boolean safeToStartVoiceBasedInteractions() {
3852+
KeyguardManager keyguard =
3853+
(KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
3854+
if (keyguard == null) {
3855+
return false;
3856+
}
3857+
3858+
return true;
3859+
}
3860+
36573861
private PowerManager.WakeLock mMediaEventWakeLock;
36583862

36593863
private static final int WAKELOCK_RELEASE_ON_FINISHED = 1980; //magic number
@@ -3671,7 +3875,14 @@ public void onSendFinished(PendingIntent pendingIntent, Intent intent,
36713875

36723876
BroadcastReceiver mKeyEventDone = new BroadcastReceiver() {
36733877
public void onReceive(Context context, Intent intent) {
3674-
if (intent.getExtras().containsKey(EXTRA_WAKELOCK_ACQUIRED)) {
3878+
if (intent == null) {
3879+
return;
3880+
}
3881+
Bundle extras = intent.getExtras();
3882+
if (extras == null) {
3883+
return;
3884+
}
3885+
if (extras.containsKey(EXTRA_WAKELOCK_ACQUIRED)) {
36753886
mMediaEventWakeLock.release();
36763887
}
36773888
}

0 commit comments

Comments
 (0)