Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 42c3e02c authored by Sandeep Siddhartha's avatar Sandeep Siddhartha
Browse files

Start using the Hotword recognition APIs

- Gets rid of all assumptions about the hotword service
- Fixes bug where the hotword detection would keep running accidentally
  even when the screen got turned off

Change-Id: Ie86c1a4f4343bdf7e61f7c21114fd3b287bd5401
parent bd31a483
Loading
Loading
Loading
Loading
+0 −3
Original line number Original line Diff line number Diff line
@@ -38,9 +38,6 @@
    <uses-permission android:name="android.permission.BIND_DEVICE_ADMIN" />
    <uses-permission android:name="android.permission.BIND_DEVICE_ADMIN" />
    <uses-permission android:name="android.permission.CHANGE_COMPONENT_ENABLED_STATE" />
    <uses-permission android:name="android.permission.CHANGE_COMPONENT_ENABLED_STATE" />


    <!-- Permission for the Hotword detector service -->
    <uses-permission android:name="com.google.android.googlequicksearchbox.SEARCH_API" />

    <application android:label="@string/app_name"
    <application android:label="@string/app_name"
        android:process="com.android.systemui"
        android:process="com.android.systemui"
        android:persistent="true" >
        android:persistent="true" >
+0 −208
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2013 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.keyguard;

import android.app.SearchManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;

import com.google.android.search.service.IHotwordService;
import com.google.android.search.service.IHotwordServiceCallback;

/**
 * Utility class with its callbacks to simplify usage of {@link IHotwordService}.
 *
 * The client is meant to be used for a single hotword detection in a session.
 * start() -> stop(); client is asked to stop & disconnect from the service.
 * start() -> onHotwordDetected(); client disconnects from the service automatically.
 */
public class HotwordServiceClient implements Handler.Callback {
    private static final String TAG = "HotwordServiceClient";
    private static final boolean DBG = true;
    private static final String ACTION_HOTWORD =
            "com.google.android.search.service.IHotwordService";

    private static final int MSG_SERVICE_CONNECTED = 0;
    private static final int MSG_SERVICE_DISCONNECTED = 1;
    private static final int MSG_HOTWORD_STARTED = 2;
    private static final int MSG_HOTWORD_STOPPED = 3;
    private static final int MSG_HOTWORD_DETECTED = 4;

    private final Context mContext;
    private final Callback mClientCallback;
    private final Handler mHandler;

    private IHotwordService mService;

    public HotwordServiceClient(Context context, Callback callback) {
        mContext = context;
        mClientCallback = callback;
        mHandler = new Handler(this);
    }

    public interface Callback {
        void onServiceConnected();
        void onServiceDisconnected();
        void onHotwordDetectionStarted();
        void onHotwordDetectionStopped();
        void onHotwordDetected(String action);
    }

    /**
     * Binds to the {@link IHotwordService} and starts hotword detection
     * when the service is connected.
     *
     * @return false if the service can't be bound to.
     */
    public synchronized boolean start() {
        if (mService != null) {
            if (DBG) Log.d(TAG, "Multiple call to start(), service was already bound");
            return true;
        } else {
            // TODO: The hotword service is currently hosted within the search app
            // so the component handling the assist intent should handle hotwording
            // as well.
            final Intent intent =
                    ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE))
                            .getAssistIntent(mContext, true, UserHandle.USER_CURRENT);
            if (intent == null) {
                return false;
            }

            Intent hotwordIntent = new Intent(ACTION_HOTWORD);
            hotwordIntent.fillIn(intent, Intent.FILL_IN_PACKAGE);
            return mContext.bindService(
                    hotwordIntent,
                   mConnection,
                   Context.BIND_AUTO_CREATE);
        }
    }

    /**
     * Unbinds from the the {@link IHotwordService}.
     */
    public synchronized void stop() {
        if (mService != null) {
            mContext.unbindService(mConnection);
            mService = null;
        }
    }

    @Override
    public boolean handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_SERVICE_CONNECTED:
                handleServiceConnected();
                break;
            case MSG_SERVICE_DISCONNECTED:
                handleServiceDisconnected();
                break;
            case MSG_HOTWORD_STARTED:
                handleHotwordDetectionStarted();
                break;
            case MSG_HOTWORD_STOPPED:
                handleHotwordDetectionStopped();
                break;
            case MSG_HOTWORD_DETECTED:
                handleHotwordDetected((String) msg.obj);
                break;
            default:
                if (DBG) Log.e(TAG, "Unhandled message");
                return false;
        }
        return true;
    }

    private void handleServiceConnected() {
        if (DBG) Log.d(TAG, "handleServiceConnected()");
        if (mClientCallback != null) mClientCallback.onServiceConnected();
        try {
            mService.requestHotwordDetection(mServiceCallback);
        } catch (RemoteException e) {
            Log.e(TAG, "Exception while registering callback", e);
            mHandler.sendEmptyMessage(MSG_SERVICE_DISCONNECTED);
        }
    }

    private void handleServiceDisconnected() {
        if (DBG) Log.d(TAG, "handleServiceDisconnected()");
        mService = null;
        if (mClientCallback != null) mClientCallback.onServiceDisconnected();
    }

    private void handleHotwordDetectionStarted() {
        if (DBG) Log.d(TAG, "handleHotwordDetectionStarted()");
        if (mClientCallback != null) mClientCallback.onHotwordDetectionStarted();
    }

    private void handleHotwordDetectionStopped() {
        if (DBG) Log.d(TAG, "handleHotwordDetectionStopped()");
        if (mClientCallback != null) mClientCallback.onHotwordDetectionStopped();
    }

    void handleHotwordDetected(final String action) {
        if (DBG) Log.d(TAG, "handleHotwordDetected()");
        if (mClientCallback != null) mClientCallback.onHotwordDetected(action);
        stop();
    }

    /**
     * Implements service connection methods.
     */
    private ServiceConnection mConnection = new ServiceConnection() {
        /**
         * Called when the service connects after calling bind().
         */
        public void onServiceConnected(ComponentName className, IBinder iservice) {
            mService = IHotwordService.Stub.asInterface(iservice);
            mHandler.sendEmptyMessage(MSG_SERVICE_CONNECTED);
        }

        /**
         * Called if the service unexpectedly disconnects. This indicates an error.
         */
        public void onServiceDisconnected(ComponentName className) {
            mService = null;
            mHandler.sendEmptyMessage(MSG_SERVICE_DISCONNECTED);
        }
    };

    /**
     * Implements the AIDL IHotwordServiceCallback interface.
     */
    private final IHotwordServiceCallback mServiceCallback = new IHotwordServiceCallback.Stub() {

        public void onHotwordDetectionStarted() {
            mHandler.sendEmptyMessage(MSG_HOTWORD_STARTED);
        }

        public void onHotwordDetectionStopped() {
            mHandler.sendEmptyMessage(MSG_HOTWORD_STOPPED);
        }

        public void onHotwordDetected(String action) {
            mHandler.obtainMessage(MSG_HOTWORD_DETECTED, action).sendToTarget();
        }
    };
}
+69 −50
Original line number Original line Diff line number Diff line
@@ -16,6 +16,7 @@
package com.android.keyguard;
package com.android.keyguard;


import android.animation.ObjectAnimator;
import android.animation.ObjectAnimator;
import android.app.PendingIntent;
import android.app.SearchManager;
import android.app.SearchManager;
import android.app.admin.DevicePolicyManager;
import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.ComponentName;
@@ -23,9 +24,12 @@ import android.content.Context;
import android.content.Intent;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Build;
import android.os.Bundle;
import android.os.PowerManager;
import android.os.PowerManager;
import android.os.UserHandle;
import android.os.UserHandle;
import android.provider.Settings;
import android.provider.Settings;
import android.speech.hotword.HotwordRecognitionListener;
import android.speech.hotword.HotwordRecognizer;
import android.telephony.TelephonyManager;
import android.telephony.TelephonyManager;
import android.util.AttributeSet;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Log;
@@ -45,7 +49,10 @@ public class KeyguardSelectorView extends LinearLayout implements KeyguardSecuri
    private static final String ASSIST_ICON_METADATA_NAME =
    private static final String ASSIST_ICON_METADATA_NAME =
        "com.android.systemui.action_assist_icon";
        "com.android.systemui.action_assist_icon";
    // Flag to enable/disable hotword detection on lock screen.
    // Flag to enable/disable hotword detection on lock screen.
    private static final boolean FLAG_HOTWORD = false;
    private static final boolean FLAG_HOTWORD = true;

    // TODO: Fix this to be non-static.
    private static HotwordRecognizer sHotwordClient;


    private KeyguardSecurityCallback mCallback;
    private KeyguardSecurityCallback mCallback;
    private GlowPadView mGlowPadView;
    private GlowPadView mGlowPadView;
@@ -57,15 +64,13 @@ public class KeyguardSelectorView extends LinearLayout implements KeyguardSecuri
    private LockPatternUtils mLockPatternUtils;
    private LockPatternUtils mLockPatternUtils;
    private SecurityMessageDisplay mSecurityMessageDisplay;
    private SecurityMessageDisplay mSecurityMessageDisplay;
    private Drawable mBouncerFrame;
    private Drawable mBouncerFrame;
    private HotwordServiceClient mHotwordClient;


    OnTriggerListener mOnTriggerListener = new OnTriggerListener() {
    OnTriggerListener mOnTriggerListener = new OnTriggerListener() {


        public void onTrigger(View v, int target) {
        public void onTrigger(View v, int target) {
            final int resId = mGlowPadView.getResourceIdForTarget(target);
            final int resId = mGlowPadView.getResourceIdForTarget(target);
            if (FLAG_HOTWORD) {
            maybeStopHotwordDetector();
            maybeStopHotwordDetector();
            }

            switch (resId) {
            switch (resId) {
                case R.drawable.ic_action_assist_generic:
                case R.drawable.ic_action_assist_generic:
                    Intent assistIntent =
                    Intent assistIntent =
@@ -128,11 +133,9 @@ public class KeyguardSelectorView extends LinearLayout implements KeyguardSecuri
        @Override
        @Override
        public void onPhoneStateChanged(int phoneState) {
        public void onPhoneStateChanged(int phoneState) {
            if (FLAG_HOTWORD) {
            if (FLAG_HOTWORD) {
                // We need to stop the hotwording when a phone call comes in
                // We need to stop hotword detection when a call state is not idle anymore.
                // TODO(sansid): This is not really needed if onPause triggers
                if (phoneState != TelephonyManager.CALL_STATE_IDLE) {
                // when we navigate away from the keyguard
                    if (DEBUG) Log.d(TAG, "Stopping due to call state not being idle");
                if (phoneState == TelephonyManager.CALL_STATE_RINGING) {
                    if (DEBUG) Log.d(TAG, "Stopping due to CALL_STATE_RINGING");
                    maybeStopHotwordDetector();
                    maybeStopHotwordDetector();
                }
                }
            }
            }
@@ -180,8 +183,8 @@ public class KeyguardSelectorView extends LinearLayout implements KeyguardSecuri
        mSecurityMessageDisplay = new KeyguardMessageArea.Helper(this);
        mSecurityMessageDisplay = new KeyguardMessageArea.Helper(this);
        View bouncerFrameView = findViewById(R.id.keyguard_selector_view_frame);
        View bouncerFrameView = findViewById(R.id.keyguard_selector_view_frame);
        mBouncerFrame = bouncerFrameView.getBackground();
        mBouncerFrame = bouncerFrameView.getBackground();
        if (FLAG_HOTWORD) {
        if (FLAG_HOTWORD && sHotwordClient == null) {
            mHotwordClient = new HotwordServiceClient(getContext(), mHotwordCallback);
            sHotwordClient = HotwordRecognizer.createHotwordRecognizer(getContext());
        }
        }
    }
    }


@@ -286,20 +289,19 @@ public class KeyguardSelectorView extends LinearLayout implements KeyguardSecuri
    @Override
    @Override
    public void onPause() {
    public void onPause() {
        KeyguardUpdateMonitor.getInstance(getContext()).removeCallback(mUpdateCallback);
        KeyguardUpdateMonitor.getInstance(getContext()).removeCallback(mUpdateCallback);
        maybeStopHotwordDetector();
    }
    }


    @Override
    @Override
    public void onResume(int reason) {
    public void onResume(int reason) {
        KeyguardUpdateMonitor.getInstance(getContext()).registerCallback(mUpdateCallback);
        KeyguardUpdateMonitor.getInstance(getContext()).registerCallback(mUpdateCallback);
        // TODO: Figure out if there's a better way to do it.
        // TODO: Figure out if there's a better way to do it.
        // Right now we don't get onPause at all, and onResume gets called
        // onResume gets called multiple times, however we are interested in
        // multiple times (even when the screen is turned off with VIEW_REVEALED)
        // the reason to figure out when to start/stop hotword detection.
        if (reason == SCREEN_ON) {
        if (reason == SCREEN_ON) {
            if (!KeyguardUpdateMonitor.getInstance(getContext()).isSwitchingUser()) {
            if (!KeyguardUpdateMonitor.getInstance(getContext()).isSwitchingUser()) {
                maybeStartHotwordDetector();
                maybeStartHotwordDetector();
            }
            }
        } else {
            maybeStopHotwordDetector();
        }
        }
    }
    }


@@ -324,15 +326,15 @@ public class KeyguardSelectorView extends LinearLayout implements KeyguardSecuri


    /**
    /**
     * Start the hotword detector if:
     * Start the hotword detector if:
     * <li> HOTWORDING_ENABLED is true and
     * <li> FLAG_HOTWORD is true and
     * <li> HotwordUnlock is initialized and
     * <li> Hotword detection is not already running and
     * <li> TelephonyManager is in CALL_STATE_IDLE
     * <li> TelephonyManager is in CALL_STATE_IDLE
     *
     *
     * If this method is called when the screen is off,
     * If this method is called when the screen is off,
     * it attempts to stop hotwording if it's running.
     * it attempts to stop hotword detection if it's running.
     */
     */
    private void maybeStartHotwordDetector() {
    private void maybeStartHotwordDetector() {
        if (FLAG_HOTWORD && mHotwordClient != null) {
        if (FLAG_HOTWORD && sHotwordClient != null) {
            if (DEBUG) Log.d(TAG, "maybeStartHotwordDetector()");
            if (DEBUG) Log.d(TAG, "maybeStartHotwordDetector()");
            // Don't start it if the screen is off or not showing
            // Don't start it if the screen is off or not showing
            PowerManager powerManager = (PowerManager) getContext().getSystemService(
            PowerManager powerManager = (PowerManager) getContext().getSystemService(
@@ -347,8 +349,13 @@ public class KeyguardSelectorView extends LinearLayout implements KeyguardSecuri
                if (DEBUG) Log.d(TAG, "Call underway, not starting");
                if (DEBUG) Log.d(TAG, "Call underway, not starting");
                return;
                return;
            }
            }
            if (!mHotwordClient.start()) {

                Log.w(TAG, "Failed to start the hotword detector");
            try {
                sHotwordClient.startRecognition(mHotwordCallback);
            } catch(Exception ex) {
                // Don't allow hotword errors to make the keyguard unusable
                Log.e(TAG, "Failed to start hotword recognition", ex);
                sHotwordClient = null;
            }
            }
        }
        }
    }
    }
@@ -357,47 +364,59 @@ public class KeyguardSelectorView extends LinearLayout implements KeyguardSecuri
     * Stop hotword detector if HOTWORDING_ENABLED is true.
     * Stop hotword detector if HOTWORDING_ENABLED is true.
     */
     */
    private void maybeStopHotwordDetector() {
    private void maybeStopHotwordDetector() {
        if (FLAG_HOTWORD && mHotwordClient != null) {
        if (FLAG_HOTWORD && sHotwordClient != null) {
            if (DEBUG) Log.d(TAG, "maybeStopHotwordDetector()");
            if (DEBUG) Log.d(TAG, "maybeStopHotwordDetector()");
            mHotwordClient.stop();
            try {
                sHotwordClient.stopRecognition();
            } catch(Exception ex) {
                // Don't allow hotword errors to make the keyguard unusable
                Log.e(TAG, "Failed to start hotword recognition", ex);
            } finally {
                sHotwordClient = null;
            }
        }
        }
    }
    }


    private final HotwordServiceClient.Callback mHotwordCallback =
    private final HotwordRecognitionListener mHotwordCallback = new HotwordRecognitionListener() {
            new HotwordServiceClient.Callback() {
        private static final String TAG = "HotwordRecognitionListener";
        private static final String TAG = "HotwordServiceClient.Callback";


        @Override
        public void onHotwordRecognitionStarted() {
        public void onServiceConnected() {
            if (DEBUG) Log.d(TAG, "onHotwordRecognitionStarted()");
            if (DEBUG) Log.d(TAG, "onServiceConnected()");
        }
        }


        @Override
        public void onHotwordRecognitionStopped() {
        public void onServiceDisconnected() {
            if (DEBUG) Log.d(TAG, "onHotwordRecognitionStopped()");
            if (DEBUG) Log.d(TAG, "onServiceDisconnected()");
        }
        }


        @Override
        public void onHotwordEvent(int eventType, Bundle eventBundle) {
        public void onHotwordDetectionStarted() {
            if (DEBUG) Log.d(TAG, "onHotwordEvent: " + eventType);
            if (DEBUG) Log.d(TAG, "onHotwordDetectionStarted()");
            if (eventType == HotwordRecognizer.EVENT_TYPE_STATE_CHANGED) {
            // TODO: Change the usage of SecurityMessageDisplay to a better visual indication.
                if (eventBundle != null && eventBundle.containsKey(HotwordRecognizer.PROMPT_TEXT)) {
            mSecurityMessageDisplay.setMessage("\"Ok Google...\"", true);
                    mSecurityMessageDisplay.setMessage(
                            eventBundle.getString(HotwordRecognizer.PROMPT_TEXT), true);
                }
            }
            }

        @Override
        public void onHotwordDetectionStopped() {
            if (DEBUG) Log.d(TAG, "onHotwordDetectionStopped()");
            // TODO: Change the usage of SecurityMessageDisplay to a better visual indication.
        }
        }


        @Override
        public void onHotwordRecognized(PendingIntent intent) {
        public void onHotwordDetected(String action) {
            if (DEBUG) Log.d(TAG, "onHotwordRecognized");
            if (DEBUG) Log.d(TAG, "onHotwordDetected(" + action + ")");
            maybeStopHotwordDetector();
            if (action != null) {
            if (intent != null) {
                Intent intent = new Intent(action);
                try {
                mActivityLauncher.launchActivity(intent, true, true, null, null);
                    intent.send();
                } catch (PendingIntent.CanceledException e) {
                    Log.w(TAG, "Failed to launch PendingIntent. Encountered CanceledException");
                }
            }
            }
            mCallback.userActivity(0);
            mCallback.userActivity(0);
            mCallback.dismiss(false);
        }

        public void onHotwordError(int errorCode) {
            if (DEBUG) Log.d(TAG, "onHotwordError: " + errorCode);
            // TODO: Inspect the error code and handle the errors appropriately
            // instead of blindly failing.
            maybeStopHotwordDetector();
        }
        }
    };
    };
}
}
+0 −35
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2013 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.google.android.search.service;

import com.google.android.search.service.IHotwordServiceCallback;

/**
 * Interface exposing hotword detector as a service.
 */
oneway interface IHotwordService {

    /**
     * Indicates a desire to start hotword detection.
     * It's best-effort and the client should rely on
     * the callbacks to figure out if hotwording was actually
     * started or not.
     *
     * @param a callback to notify of hotword events.
     */
    void requestHotwordDetection(in IHotwordServiceCallback callback);
}
+0 −34
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2013 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.google.android.search.service;

/**
 * Interface implemented by users of Hotword service to get callbacks
 * for hotword events.
 */
oneway interface IHotwordServiceCallback {

    /** Hotword detection start/stop callbacks */
    void onHotwordDetectionStarted();
    void onHotwordDetectionStopped();

    /**
     * Called back when hotword is detected.
     * The action tells the client what action to take, post hotword-detection.
     */
    void onHotwordDetected(in String action);
}